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

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:

  • 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:

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:

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:

// 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:

  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():
    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
  • 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