feat(ios): add markProcessed share-completion API (Phase 1D)

Add a native iOS markProcessed(shareId) method to the SharedImage plugin
that records an explicit completion signal in App Group UserDefaults
(sharedPhotoProcessedShareId) and logs "[ShareTarget] shareId=<id> marked
processed". The API is exposed to TypeScript via the plugin definitions and
a web no-op stub.

Deliberately does not delete files or clear share metadata (cleanup is
deferred to a later phase), is not yet called from application code, and
does not touch Android. Documents the share-target lifecycle and plugin API
in doc/share-target-ios-audit.md.
This commit is contained in:
Jose Olarte III
2026-06-30 11:00:52 +08:00
parent 125406a54c
commit 981f8b77d0
5 changed files with 152 additions and 1 deletions

View File

@@ -0,0 +1,86 @@
# iOS Share Target Audit
**Date:** 2026-06-30
**Platform:** iOS only (Android unchanged)
**Status:** Phase 1D complete — explicit share completion API added (not yet called)
## Purpose
This document tracks the native iOS share-target lifecycle and the
Capacitor API surface exposed to the web/TypeScript layer. It is updated as
each phase lands.
## Lifecycle Overview
1. The Share Extension (`ShareViewController.swift`) receives an image, writes
it to the App Group container, records metadata in App Group
`UserDefaults`, assigns a `shareId` (UUID), sets the `sharedPhotoReady`
flag, and opens the main app via `timesafari://`.
2. The main app reads the shared image through the `SharedImage` Capacitor
plugin.
3. The application layer processes the image, then (in a future phase) signals
explicit completion via `markProcessed(shareId)`.
## App Group Storage Keys
Stored in `UserDefaults(suiteName: "group.app.trentlarson.timesafari.share")`:
| Key | Set by | Meaning |
| --- | --- | --- |
| `sharedPhotoFileName` | Share Extension | Original display filename |
| `sharedPhotoFilePath` | Share Extension | On-disk filename in App Group container |
| `sharedPhotoShareId` | Share Extension | Current pending share identifier (UUID) |
| `sharedPhotoReady` | Share Extension | Flag indicating a share is ready |
| `sharedPhotoProcessedShareId` | Main app (`markProcessed`) | Last shareId explicitly marked processed (Phase 1D) |
## Capacitor API (`SharedImage` plugin)
The plugin is registered as `SharedImage` and is implemented in:
- Native iOS: `ios/App/App/SharedImagePlugin.swift` (delegates to
`ios/App/App/SharedImageUtility.swift`)
- Web stub: `src/plugins/SharedImagePlugin.web.ts`
- TypeScript definitions: `src/plugins/definitions.ts`
### `getSharedImage(): Promise<SharedImageResult | null>`
Returns the shared image as `{ base64, fileName }`, or `null` if none exists.
Read-only: native metadata and the file are left intact after retrieval
(Phase 1C).
### `hasSharedImage(): Promise<{ hasImage: boolean }>`
Returns whether a shared image file currently exists, without reading it.
### `markProcessed(options: { shareId: string }): Promise<{ success: boolean }>` — Phase 1D
Explicit completion signal from the application layer for a given share.
- **Input:** `shareId` — the identifier reported by `getSharedImage()`.
Must be a non-empty string; otherwise the call rejects.
- **Behavior (iOS):** Records `shareId` in App Group `UserDefaults` under
`sharedPhotoProcessedShareId` and resolves with `{ success: true }`.
- **Does NOT delete files:** The stored image file and share metadata are
intentionally left intact. File cleanup is deferred to a later phase.
- **Web:** No-op; resolves with `{ success: false }`.
- **Logging:** Emits `[ShareTarget] shareId=<id> marked processed` on success.
> Note (Phase 1D): `markProcessed` is implemented and exposed to TypeScript
> but is **not yet called** from application code. Wiring it into the
> processing flow is a later phase.
## Native Logging Reference
Relevant `[ShareTarget]` log lines emitted on iOS:
- `[ShareTarget] shareId=<id> retrieved`
- `[ShareTarget] shareId=<id> left intact after retrieval`
- `[ShareTarget] shareId=<id> marked processed` (Phase 1D)
## Out of Scope
- Android: no changes. The Android plugin
(`android/app/src/main/java/app/timesafari/sharedimage/SharedImagePlugin.java`)
does not implement `markProcessed`.
- File deletion / cleanup of processed shares.
- Calling `markProcessed` from application code.

View File

@@ -24,7 +24,8 @@ public class SharedImagePlugin: CAPPlugin, CAPBridgedPlugin {
public var pluginMethods: [CAPPluginMethod] { public var pluginMethods: [CAPPluginMethod] {
return [ return [
CAPPluginMethod(#selector(getSharedImage(_:)), returnType: .promise), CAPPluginMethod(#selector(getSharedImage(_:)), returnType: .promise),
CAPPluginMethod(#selector(hasSharedImage(_:)), returnType: .promise) CAPPluginMethod(#selector(hasSharedImage(_:)), returnType: .promise),
CAPPluginMethod(#selector(markProcessed(_:)), returnType: .promise)
] ]
} }
@@ -62,5 +63,22 @@ public class SharedImagePlugin: CAPPlugin, CAPBridgedPlugin {
"hasImage": hasImage "hasImage": hasImage
]) ])
} }
/**
* Mark a share as processed by the application layer (Phase 1D)
* Records an explicit completion signal for the given shareId.
* Does NOT delete the stored image file - file cleanup is deferred to a later phase.
*/
@objc public func markProcessed(_ call: CAPPluginCall) {
guard let shareId = call.getString("shareId"), !shareId.isEmpty else {
call.reject("markProcessed requires a non-empty 'shareId'")
return
}
let success = SharedImageUtility.markProcessed(shareId: shareId)
call.resolve([
"success": success
])
}
} }

View File

@@ -15,6 +15,7 @@ public class SharedImageUtility {
private static let sharedPhotoFilePathKey = "sharedPhotoFilePath" private static let sharedPhotoFilePathKey = "sharedPhotoFilePath"
private static let sharedPhotoShareIdKey = "sharedPhotoShareId" private static let sharedPhotoShareIdKey = "sharedPhotoShareId"
private static let sharedPhotoReadyKey = "sharedPhotoReady" private static let sharedPhotoReadyKey = "sharedPhotoReady"
private static let sharedPhotoProcessedShareIdKey = "sharedPhotoProcessedShareId"
/// Get the App Group container URL for accessing shared files /// Get the App Group container URL for accessing shared files
private static var appGroupContainerURL: URL? { private static var appGroupContainerURL: URL? {
@@ -78,6 +79,32 @@ public class SharedImageUtility {
return ["base64": base64String, "fileName": fileName] return ["base64": base64String, "fileName": fileName]
} }
/**
* Mark a share as processed by the web/application layer (Phase 1D).
*
* Records the processed shareId in the App Group UserDefaults so that
* later phases can decide whether retrieval/cleanup should be skipped.
* This is an explicit completion signal: it does NOT delete the stored
* image file or clear the share metadata. File cleanup is deferred to a
* later phase.
*
* @param shareId The share identifier reported by getSharedImage()
* @returns true if the processed marker was recorded, false otherwise
*/
@discardableResult
static func markProcessed(shareId: String) -> Bool {
guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else {
print("[ShareTarget] shareId=\(shareId) markProcessed failed: UserDefaults unavailable for app group")
return false
}
userDefaults.set(shareId, forKey: sharedPhotoProcessedShareIdKey)
userDefaults.synchronize()
print("[ShareTarget] shareId=\(shareId) marked processed")
return true
}
/** /**
* Check if shared image exists without reading it * Check if shared image exists without reading it
* *

View File

@@ -18,4 +18,11 @@ export class SharedImagePluginWeb
async hasSharedImage(): Promise<{ hasImage: boolean }> { async hasSharedImage(): Promise<{ hasImage: boolean }> {
return { hasImage: false }; return { hasImage: false };
} }
async markProcessed(_options: {
shareId: string;
}): Promise<{ success: boolean }> {
// Web platform doesn't support native sharing - no-op
return { success: false };
}
} }

View File

@@ -20,4 +20,17 @@ export interface SharedImagePlugin {
* Useful for quick checks before calling getSharedImage() * Useful for quick checks before calling getSharedImage()
*/ */
hasSharedImage(): Promise<{ hasImage: boolean }>; hasSharedImage(): Promise<{ hasImage: boolean }>;
/**
* Mark a share as explicitly processed by the application layer (Phase 1D, iOS only).
*
* Records a completion signal for the given shareId on the native side.
* This does NOT delete the stored image file or clear share metadata;
* file cleanup is deferred to a later phase.
*
* On web this is a no-op that resolves with success=false.
*
* @param options.shareId The share identifier reported by getSharedImage()
*/
markProcessed(options: { shareId: string }): Promise<{ success: boolean }>;
} }