WIP: iOS Share Target Reliability #235

Draft
jose wants to merge 31 commits from fix/ios-share-target-reliability into master
5 changed files with 96 additions and 1 deletions
Showing only changes of commit 0c91b687f9 - Show all commits

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

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

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