WIP: iOS Share Target Reliability #235
@@ -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
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user