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:
86
doc/share-target-ios-audit.md
Normal file
86
doc/share-target-ios-audit.md
Normal 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.
|
||||||
@@ -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
|
||||||
|
])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }>;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user