17 KiB
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:
- ✅
CAPPluginbase 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) - ✅
@objcannotations: 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:
- ✅
Pluginbase class: Available in API 21+ (Capacitor requirement) - ✅
PluginCall: Available in API 21+ (Capacitor requirement) - ✅
SharedPreferences: Available since API 1 (works on all Android versions) - ✅
@CapacitorPluginannotation: Available in API 21+ (Capacitor requirement) - ✅
@PluginMethodannotation: 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:
- Timing Issues: Requires polling with exponential backoff to wait for file creation
- Race Conditions: File may not exist when JS checks, or may be read multiple times
- File Management: Need to delete temp files after reading to prevent re-processing
- Platform Differences: Different directories (Documents vs Data) add complexity
- Error Handling: File I/O errors can be hard to debug
- 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:
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
base64andfileNamekeys - 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:
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_NAMEconstant - 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:
// 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:
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:
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:
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:
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:
ios/App/App/SharedImagePlugin.swift- iOS plugin implementationandroid/app/src/main/java/app/timesafari/sharedimage/SharedImagePlugin.java- Android pluginsrc/plugins/SharedImagePlugin.ts- TypeScript plugin registrationsrc/plugins/SharedImagePlugin.web.ts- Web fallback implementationsrc/plugins/definitions.ts- TypeScript type definitions
Files to Modify:
ios/App/App/AppDelegate.swift- Remove temp file writingandroid/app/src/main/java/app/timesafari/MainActivity.java- Remove temp file writing, add SharedPreferencessrc/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:
- App Launch: Check in
checkForSharedImageAndNavigate()(already exists) - App Becomes Active: Check in
appStateChangelistener (already exists) - 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
nullimmediately
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:
- Share from Photos app → Verify image appears in app
- Share while app is backgrounded → Verify image appears when app becomes active
- Share while app is closed → Verify image appears on app launch
- Multiple rapid shares → Verify only latest image is processed
- Share then close app before processing → Verify image persists
- 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():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:
- Create plugin files
- Register Android plugin in MainActivity
- Update TypeScript code
- Test on iOS simulator
- Test on Android emulator
- Test on physical devices
- Remove temp file code
- 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)
- Create iOS plugin file
- Create Android plugin file
- Create TypeScript definitions
- Register Android plugin
- Test plugins independently (don't use in main code yet)
Phase 2: Update JS Integration (Breaking)
- Create TypeScript plugin wrapper
- Update
checkAndStoreNativeSharedImage()to use plugin - Remove temp file reading logic
- Test on both platforms
Phase 3: Cleanup Native Code (Breaking)
- Remove temp file writing from iOS AppDelegate
- Remove temp file writing from Android MainActivity
- Update to use SharedPreferences on Android
- Test thoroughly
Phase 4: Final Cleanup
- Remove
pollForFileExistence()function - Remove Filesystem imports related to temp files
- Update comments and documentation
- Final testing
Rollback Plan
If issues arise:
- Revert JS changes to use temp file approach
- Re-enable temp file writing in native code
- Keep plugins for future migration attempt
- 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_SENDandACTION_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
- Existing plugin example:
SafeAreaPlugin.java - Current temp file implementation:
main.capacitor.tslines 166-271 - iOS AppDelegate:
ios/App/App/AppDelegate.swift - Android MainActivity:
android/app/src/main/java/app/timesafari/MainActivity.java