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.
This commit is contained in:
Jose Olarte III
2026-06-30 22:17:37 +08:00
parent 7093f38e09
commit 0c91b687f9
5 changed files with 96 additions and 1 deletions

View File

@@ -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
])
}
}

View File

@@ -118,6 +118,55 @@ public class SharedImageUtility {
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
*

View File

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

View File

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

View File

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