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 }>; }