Files
crowd-funder-for-time-pwa/doc/shared-image-plugin-implementation-plan.md

529 lines
17 KiB
Markdown

# Shared Image Plugin Implementation Plan
**Date:** 2025-12-03 15:40:38 PST
**Status:** Planning
**Goal:** Replace temp file approach with native Capacitor plugins for iOS and Android
## Minimum OS Version Compatibility Analysis
### Current Project Configuration:
- **iOS Deployment Target**: 13.0 (Podfile and Xcode project)
- **Android minSdkVersion**: 23 (API 23 - Android 6.0 Marshmallow) ✅ **Upgraded**
- **Capacitor Version**: 6.2.0
### Capacitor 6 Requirements:
- **iOS**: Requires iOS 13.0+ ✅ **Compatible** (current: 13.0)
- **Android**: Requires API 23+ ✅ **Compatible** (current: API 23)
### Plugin API Compatibility:
#### iOS Plugin APIs:
-`CAPPlugin` base class: Available in iOS 13.0+ (Capacitor requirement)
-`CAPPluginCall`: Available in iOS 13.0+ (Capacitor requirement)
-`UserDefaults(suiteName:)`: Available since iOS 8.0 (well below iOS 13.0)
-`@objc` annotations: Available since iOS 8.0
- ✅ Swift 5.0: Compatible with iOS 13.0+
**Conclusion**: iOS 13.0 is fully compatible with the plugin implementation. **No iOS version update required.**
#### Android Plugin APIs:
-`Plugin` base class: Available in API 21+ (Capacitor requirement)
-`PluginCall`: Available in API 21+ (Capacitor requirement)
-`SharedPreferences`: Available since API 1 (works on all Android versions)
-`@CapacitorPlugin` annotation: Available in API 21+ (Capacitor requirement)
-`@PluginMethod` annotation: Available in API 21+ (Capacitor requirement)
**Conclusion**: Android API 23 is fully compatible with the plugin implementation and officially meets Capacitor 6 requirements. ✅ **No Android version concerns.**
### Share Extension Compatibility:
- **iOS Share Extension**: Uses same deployment target as main app (iOS 13.0)
- **App Group**: Available since iOS 8.0, fully compatible
- No additional version requirements for share extension functionality
## Overview
This document outlines the migration from the current temp file approach to implementing dedicated Capacitor plugins for handling shared images. This will eliminate file I/O, polling, and timing issues, providing a more direct and reliable native-to-JS bridge.
## Current Implementation Issues
### Temp File Approach Problems:
1. **Timing Issues**: Requires polling with exponential backoff to wait for file creation
2. **Race Conditions**: File may not exist when JS checks, or may be read multiple times
3. **File Management**: Need to delete temp files after reading to prevent re-processing
4. **Platform Differences**: Different directories (Documents vs Data) add complexity
5. **Error Handling**: File I/O errors can be hard to debug
6. **Performance**: File system operations are slower than direct native calls
## Proposed Solution: Capacitor Plugins
### Benefits:
- ✅ Direct native-to-JS communication (no file I/O)
- ✅ Synchronous/async method calls (no polling needed)
- ✅ Type-safe TypeScript interfaces
- ✅ Better error handling and debugging
- ✅ Lower latency
- ✅ More maintainable and follows Capacitor best practices
## Implementation Layout
### 1. iOS Plugin Implementation
#### 1.1 Create iOS Plugin File
**Location:** `ios/App/App/SharedImagePlugin.swift`
**Structure:**
```swift
import Foundation
import Capacitor
@objc(SharedImagePlugin)
public class SharedImagePlugin: CAPPlugin {
private let appGroupIdentifier = "group.app.timesafari.share"
@objc func getSharedImage(_ call: CAPPluginCall) {
// Read from App Group UserDefaults
// Return base64 and fileName
// Clear data after reading
}
@objc func hasSharedImage(_ call: CAPPluginCall) {
// Check if shared image exists without reading it
// Useful for quick checks
}
}
```
**Key Points:**
- Use existing `getSharedImageData()` logic from AppDelegate
- Return data as JSObject with `base64` and `fileName` keys
- Clear UserDefaults after reading to prevent re-reading
- Handle errors gracefully with `call.reject()`
- **Version Compatibility**: Works with iOS 13.0+ (current deployment target)
#### 1.2 Register Plugin in iOS
**Location:** `ios/App/App/AppDelegate.swift`
**Changes:**
- Remove `writeSharedImageToTempFile()` method
- Remove temp file writing from `application(_:open:options:)`
- Remove temp file writing from `checkForSharedImageOnActivation()`
- Keep `getSharedImageData()` method (or move to plugin)
- Plugin auto-registers via Capacitor's plugin system
**Note:** Capacitor plugins are auto-discovered if they follow naming conventions and are in the app bundle.
### 2. Android Plugin Implementation
#### 2.1 Create Android Plugin File
**Location:** `android/app/src/main/java/app/timesafari/sharedimage/SharedImagePlugin.java`
**Structure:**
```java
package app.timesafari.sharedimage;
import com.getcapacitor.JSObject;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginMethod;
import com.getcapacitor.annotation.CapacitorPlugin;
@CapacitorPlugin(name = "SharedImage")
public class SharedImagePlugin extends Plugin {
@PluginMethod
public void getSharedImage(PluginCall call) {
// Read from SharedPreferences or Intent extras
// Return base64 and fileName
// Clear data after reading
}
@PluginMethod
public void hasSharedImage(PluginCall call) {
// Check if shared image exists without reading it
}
}
```
**Key Points:**
- Use SharedPreferences to store shared image data between share intent and plugin call
- Store base64 and fileName when processing share intent
- Read and clear in `getSharedImage()` method
- Handle Intent extras if app was just launched
- **Version Compatibility**: Works with Android API 22+ (current minSdkVersion)
#### 2.2 Update MainActivity
**Location:** `android/app/src/main/java/app/timesafari/MainActivity.java`
**Changes:**
- Remove `writeSharedImageToTempFile()` method
- Remove `TEMP_FILE_NAME` constant
- Update `processSharedImage()` to store in SharedPreferences instead of file
- Register plugin: `registerPlugin(SharedImagePlugin.class);`
- Store shared image data in SharedPreferences when processing share intent
**SharedPreferences Approach:**
```java
// In processSharedImage():
SharedPreferences prefs = getSharedPreferences("shared_image", MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putString("base64", base64String);
editor.putString("fileName", actualFileName);
editor.putBoolean("hasSharedImage", true);
editor.apply();
```
### 3. TypeScript/JavaScript Integration
#### 3.1 Create TypeScript Plugin Definition
**Location:** `src/plugins/SharedImagePlugin.ts` (new file)
**Structure:**
```typescript
import { registerPlugin } from '@capacitor/core';
export interface SharedImageResult {
base64: string;
fileName: string;
}
export interface SharedImagePlugin {
getSharedImage(): Promise<SharedImageResult | null>;
hasSharedImage(): Promise<{ hasImage: boolean }>;
}
const SharedImage = registerPlugin<SharedImagePlugin>('SharedImage', {
web: () => import('./SharedImagePlugin.web').then(m => new m.SharedImagePluginWeb()),
});
export * from './definitions';
export { SharedImage };
```
#### 3.2 Create Web Implementation (for development)
**Location:** `src/plugins/SharedImagePlugin.web.ts` (new file)
**Structure:**
```typescript
import { WebPlugin } from '@capacitor/core';
import type { SharedImagePlugin, SharedImageResult } from './definitions';
export class SharedImagePluginWeb extends WebPlugin implements SharedImagePlugin {
async getSharedImage(): Promise<SharedImageResult | null> {
// Return null for web platform
return null;
}
async hasSharedImage(): Promise<{ hasImage: boolean }> {
return { hasImage: false };
}
}
```
#### 3.3 Create Type Definitions
**Location:** `src/plugins/definitions.ts` (new file)
**Structure:**
```typescript
export interface SharedImageResult {
base64: string;
fileName: string;
}
export interface SharedImagePlugin {
getSharedImage(): Promise<SharedImageResult | null>;
hasSharedImage(): Promise<{ hasImage: boolean }>;
}
```
#### 3.4 Update main.capacitor.ts
**Location:** `src/main.capacitor.ts`
**Changes:**
- Remove `pollForFileExistence()` function
- Remove temp file reading logic from `checkAndStoreNativeSharedImage()`
- Replace with direct plugin call:
```typescript
async function checkAndStoreNativeSharedImage(): Promise<{
success: boolean;
fileName?: string;
}> {
if (isProcessingSharedImage) {
logger.debug("[Main] ⏸️ Shared image processing already in progress, skipping");
return { success: false };
}
isProcessingSharedImage = true;
try {
if (!Capacitor.isNativePlatform() ||
(Capacitor.getPlatform() !== "ios" && Capacitor.getPlatform() !== "android")) {
isProcessingSharedImage = false;
return { success: false };
}
// Direct plugin call - no polling needed!
const { SharedImage } = await import('./plugins/SharedImagePlugin');
const result = await SharedImage.getSharedImage();
if (result && result.base64) {
await storeSharedImageInTempDB(result.base64, result.fileName);
isProcessingSharedImage = false;
return { success: true, fileName: result.fileName };
}
isProcessingSharedImage = false;
return { success: false };
} catch (error) {
logger.error("[Main] Error checking for native shared image:", error);
isProcessingSharedImage = false;
return { success: false };
}
}
```
**Remove:**
- `pollForFileExistence()` function (lines 71-98)
- All Filesystem plugin imports related to temp file reading
- Temp file path constants and directory logic
### 4. Data Flow Comparison
#### Current (Temp File) Flow:
```
Share Extension/Intent
Native writes temp file
JS polls for file existence (with retries)
JS reads file via Filesystem plugin
JS parses JSON
JS deletes temp file
JS stores in temp DB
```
#### New (Plugin) Flow:
```
Share Extension/Intent
Native stores in UserDefaults/SharedPreferences
JS calls plugin.getSharedImage()
Native reads and clears data
Native returns data directly
JS stores in temp DB
```
## File Changes Summary
### New Files to Create:
1. `ios/App/App/SharedImagePlugin.swift` - iOS plugin implementation
2. `android/app/src/main/java/app/timesafari/sharedimage/SharedImagePlugin.java` - Android plugin
3. `src/plugins/SharedImagePlugin.ts` - TypeScript plugin registration
4. `src/plugins/SharedImagePlugin.web.ts` - Web fallback implementation
5. `src/plugins/definitions.ts` - TypeScript type definitions
### Files to Modify:
1. `ios/App/App/AppDelegate.swift` - Remove temp file writing
2. `android/app/src/main/java/app/timesafari/MainActivity.java` - Remove temp file writing, add SharedPreferences
3. `src/main.capacitor.ts` - Replace temp file logic with plugin calls
### Files to Remove:
- No files need to be deleted, but code will be removed from existing files
## Implementation Considerations
### 1. Data Storage Strategy
#### iOS:
- **Current**: App Group UserDefaults (already working)
- **Plugin**: Read from same UserDefaults, no changes needed
- **Clearing**: Clear immediately after reading in plugin method
#### Android:
- **Current**: Temp file in app's internal files directory
- **New**: SharedPreferences (persistent key-value store)
- **Alternative**: Could use Intent extras if app is launched fresh, but SharedPreferences is more reliable for backgrounded apps
### 2. Timing and Lifecycle
#### When to Check for Shared Images:
1. **App Launch**: Check in `checkForSharedImageAndNavigate()` (already exists)
2. **App Becomes Active**: Check in `appStateChange` listener (already exists)
3. **Deep Link**: Check in `handleDeepLink()` for empty path URLs (already exists)
#### Plugin Call Timing:
- Plugin calls are synchronous from JS perspective
- No polling needed - native side handles data availability
- If no data exists, plugin returns `null` immediately
### 3. Error Handling
#### Plugin Error Scenarios:
- **No shared image**: Return `null` (not an error)
- **Data corruption**: Return error via `call.reject()`
- **Missing permissions**: Return error (shouldn't happen with App Group/SharedPreferences)
#### JS Error Handling:
- Wrap plugin calls in try-catch
- Log errors appropriately
- Don't crash app if plugin fails
### 4. Backward Compatibility
#### Migration Path:
- Keep temp file code temporarily (commented out) for rollback
- Test thoroughly on both platforms
- Remove temp file code after verification
### 5. Testing Considerations
#### Test Cases:
1. **Share from Photos app** → Verify image appears in app
2. **Share while app is backgrounded** → Verify image appears when app becomes active
3. **Share while app is closed** → Verify image appears on app launch
4. **Multiple rapid shares** → Verify only latest image is processed
5. **Share then close app before processing** → Verify image persists
6. **Share then clear app data** → Verify graceful handling
#### Edge Cases:
- Very large images (memory concerns)
- Multiple images shared simultaneously
- App killed by OS before processing
- Network interruptions during processing
### 6. Performance Considerations
#### Benefits:
- **Latency**: Direct calls vs file I/O (faster)
- **CPU**: No polling overhead
- **Memory**: No temp file storage
- **Battery**: Less file system activity
#### Potential Issues:
- Large base64 strings in memory (same as current approach)
- UserDefaults/SharedPreferences size limits (shouldn't be an issue for single image)
### 7. Type Safety
#### TypeScript Benefits:
- Full type checking for plugin methods
- Autocomplete in IDE
- Compile-time error checking
- Better developer experience
### 8. Plugin Registration
#### iOS:
- Capacitor auto-discovers plugins via naming convention
- Ensure plugin is in app target (not extension target)
- No manual registration needed in AppDelegate
#### Android:
- Register in `MainActivity.onCreate()`:
```java
registerPlugin(SharedImagePlugin.class);
```
### 9. Capacitor Version Compatibility
#### Check Current Version:
- Verify Capacitor version supports custom plugins
- Ensure plugin API hasn't changed
- Test with current Capacitor version first
### 10. Build and Deployment
#### Build Steps:
1. Create plugin files
2. Register Android plugin in MainActivity
3. Update TypeScript code
4. Test on iOS simulator
5. Test on Android emulator
6. Test on physical devices
7. Remove temp file code
8. Update documentation
#### Deployment:
- No changes to build scripts needed
- No changes to CI/CD needed
- No changes to app configuration needed
## Migration Steps
### Phase 1: Create Plugins (Non-Breaking)
1. Create iOS plugin file
2. Create Android plugin file
3. Create TypeScript definitions
4. Register Android plugin
5. Test plugins independently (don't use in main code yet)
### Phase 2: Update JS Integration (Breaking)
1. Create TypeScript plugin wrapper
2. Update `checkAndStoreNativeSharedImage()` to use plugin
3. Remove temp file reading logic
4. Test on both platforms
### Phase 3: Cleanup Native Code (Breaking)
1. Remove temp file writing from iOS AppDelegate
2. Remove temp file writing from Android MainActivity
3. Update to use SharedPreferences on Android
4. Test thoroughly
### Phase 4: Final Cleanup
1. Remove `pollForFileExistence()` function
2. Remove Filesystem imports related to temp files
3. Update comments and documentation
4. Final testing
## Rollback Plan
If issues arise:
1. Revert JS changes to use temp file approach
2. Re-enable temp file writing in native code
3. Keep plugins for future migration attempt
4. Document issues encountered
## Success Criteria
✅ Plugin methods work on both iOS and Android
✅ No polling or file I/O needed
✅ Shared images appear correctly in app
✅ No memory leaks or performance issues
✅ Error handling works correctly
✅ All test cases pass
✅ Code is cleaner and more maintainable
## Additional Notes
### iOS App Group:
- Current App Group ID: `group.app.timesafari.share`
- Ensure plugin has access to same App Group
- Share Extension already writes to this App Group
### Android Share Intent:
- Current implementation handles `ACTION_SEND` and `ACTION_SEND_MULTIPLE`
- SharedPreferences key: `shared_image` (or similar)
- Store both base64 and fileName
### Future Enhancements:
- Consider adding event listeners for real-time notifications
- Could add method to clear shared image without reading
- Could add method to get image metadata without full data
## References
- [Capacitor Plugin Development Guide](https://capacitorjs.com/docs/plugins)
- Existing plugin example: `SafeAreaPlugin.java`
- Current temp file implementation: `main.capacitor.ts` lines 166-271
- iOS AppDelegate: `ios/App/App/AppDelegate.swift`
- Android MainActivity: `android/app/src/main/java/app/timesafari/MainActivity.java`