From 0c91b687f9c70140e5d53749075b15c4b02e2e9b Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Tue, 30 Jun 2026 22:17:37 +0800 Subject: [PATCH] feat(ios): clean up native share storage after import (Phase 1F) Add SharedImageUtility.clearProcessedShare() to permanently delete the shared image file and remove all App Group metadata (sharedPhotoFileName, sharedPhotoFilePath, sharedPhotoShareId, sharedPhotoReady, sharedPhotoProcessedShareId). The helper is idempotent and logs cleanup started/completed plus a warning on file-deletion failure. Expose it through the SharedImage plugin with a web no-op stub. Wire cleanup into the import flow so it runs only after both storeSharedImageInTempDB() and markProcessed() succeed; a cleanup failure is logged but does not fail the already-successful import. After cleanup a subsequent getSharedImage() finds no pending share. iOS only; Android, the Share Extension, and Phase 1E duplicate suppression are unchanged. --- ios/App/App/SharedImagePlugin.swift | 15 ++++++++- ios/App/App/SharedImageUtility.swift | 49 ++++++++++++++++++++++++++++ src/main.capacitor.ts | 16 +++++++++ src/plugins/SharedImagePlugin.web.ts | 5 +++ src/plugins/definitions.ts | 12 +++++++ 5 files changed, 96 insertions(+), 1 deletion(-) diff --git a/ios/App/App/SharedImagePlugin.swift b/ios/App/App/SharedImagePlugin.swift index c5966fa3..85ac0db8 100644 --- a/ios/App/App/SharedImagePlugin.swift +++ b/ios/App/App/SharedImagePlugin.swift @@ -25,7 +25,8 @@ public class SharedImagePlugin: CAPPlugin, CAPBridgedPlugin { return [ CAPPluginMethod(#selector(getSharedImage(_:)), returnType: .promise), CAPPluginMethod(#selector(hasSharedImage(_:)), returnType: .promise), - CAPPluginMethod(#selector(markProcessed(_:)), returnType: .promise) + CAPPluginMethod(#selector(markProcessed(_:)), returnType: .promise), + CAPPluginMethod(#selector(clearProcessedShare(_:)), returnType: .promise) ] } @@ -82,5 +83,17 @@ public class SharedImagePlugin: CAPPlugin, CAPBridgedPlugin { "success": success ]) } + + /** + * Permanently remove the currently processed share from App Group storage (Phase 1F) + * Deletes the image file and all share metadata. Idempotent. + * Intended to be called only after successful processing and markProcessed(). + */ + @objc public func clearProcessedShare(_ call: CAPPluginCall) { + let success = SharedImageUtility.clearProcessedShare() + call.resolve([ + "success": success + ]) + } } diff --git a/ios/App/App/SharedImageUtility.swift b/ios/App/App/SharedImageUtility.swift index defacc51..088727a0 100644 --- a/ios/App/App/SharedImageUtility.swift +++ b/ios/App/App/SharedImageUtility.swift @@ -117,6 +117,55 @@ public class SharedImageUtility { print("[ShareTarget] shareId=\(shareId) marked processed") return true } + + /** + * Permanently remove the currently processed share from App Group storage (Phase 1F). + * + * Deletes the shared image file (if present) and removes all share metadata + * keys. Idempotent: missing files or keys are treated as success. Intended + * to be called only after the share has been successfully handed off to the + * app and marked processed. + * + * @returns true if cleanup completed without error, false if file deletion failed + */ + @discardableResult + static func clearProcessedShare() -> Bool { + let userDefaults = UserDefaults(suiteName: appGroupIdentifier) + let shareId = userDefaults?.string(forKey: sharedPhotoShareIdKey) + ?? userDefaults?.string(forKey: sharedPhotoProcessedShareIdKey) + ?? "unknown" + + print("[ShareTarget] shareId=\(shareId) cleanup started") + + var success = true + + // Delete the shared image file if it exists + if let filePath = userDefaults?.string(forKey: sharedPhotoFilePathKey), + let containerURL = appGroupContainerURL { + let fileURL = containerURL.appendingPathComponent(filePath) + if FileManager.default.fileExists(atPath: fileURL.path) { + do { + try FileManager.default.removeItem(at: fileURL) + } catch { + success = false + print("[ShareTarget] shareId=\(shareId) cleanup warning: failed to delete file error=\(error.localizedDescription)") + } + } + } + + // Remove all share metadata keys (removeObject is a no-op when absent) + if let userDefaults = userDefaults { + userDefaults.removeObject(forKey: sharedPhotoFileNameKey) + userDefaults.removeObject(forKey: sharedPhotoFilePathKey) + userDefaults.removeObject(forKey: sharedPhotoShareIdKey) + userDefaults.removeObject(forKey: sharedPhotoReadyKey) + userDefaults.removeObject(forKey: sharedPhotoProcessedShareIdKey) + userDefaults.synchronize() + } + + print("[ShareTarget] shareId=\(shareId) cleanup completed") + return success + } /** * Check if shared image exists without reading it diff --git a/src/main.capacitor.ts b/src/main.capacitor.ts index 37972586..3ded2082 100644 --- a/src/main.capacitor.ts +++ b/src/main.capacitor.ts @@ -196,9 +196,11 @@ async function checkAndStoreNativeSharedImage(): Promise<{ // Signal explicit completion only after successful processing. // iOS-only: the markProcessed API is not implemented on Android. if (shareId && Capacitor.getPlatform() === "ios") { + let marked = false; try { logger.info(`[Main] markProcessed invoked shareId=${shareId}`); await SharedImage.markProcessed({ shareId }); + marked = true; } catch (markError) { // A failure to mark processed must not fail the (already successful) // share processing; the share simply remains available for retry. @@ -207,6 +209,20 @@ async function checkAndStoreNativeSharedImage(): Promise<{ markError, ); } + + // Phase 1F: native cleanup ONLY after both processing and markProcessed + // succeed. A cleanup failure is logged but does not fail the import, + // since the image has already been handed off to the app. + if (marked) { + try { + await SharedImage.clearProcessedShare(); + } catch (cleanupError) { + logger.warn( + `[Main] cleanup failed shareId=${shareId}:`, + cleanupError, + ); + } + } } isProcessingSharedImage = false; diff --git a/src/plugins/SharedImagePlugin.web.ts b/src/plugins/SharedImagePlugin.web.ts index 29ac1d1d..61cf533c 100644 --- a/src/plugins/SharedImagePlugin.web.ts +++ b/src/plugins/SharedImagePlugin.web.ts @@ -25,4 +25,9 @@ export class SharedImagePluginWeb // Web platform doesn't support native sharing - no-op return { success: false }; } + + async clearProcessedShare(): Promise<{ success: boolean }> { + // Web platform doesn't support native sharing - no-op + return { success: false }; + } } diff --git a/src/plugins/definitions.ts b/src/plugins/definitions.ts index a582b8bf..13aa70a3 100644 --- a/src/plugins/definitions.ts +++ b/src/plugins/definitions.ts @@ -38,4 +38,16 @@ export interface SharedImagePlugin { * @param options.shareId The share identifier reported by getSharedImage() */ markProcessed(options: { shareId: string }): Promise<{ success: boolean }>; + + /** + * Permanently remove the currently processed share from native storage + * (Phase 1F, iOS only). + * + * Deletes the shared image file and all share metadata. Idempotent: missing + * files/keys resolve successfully. Should only be called after the share has + * been successfully processed AND marked processed. + * + * On web this is a no-op that resolves with success=false. + */ + clearProcessedShare(): Promise<{ success: boolean }>; }