529 lines
17 KiB
Markdown
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`
|
|
|