feat(ios): add share ID tracking for share target (Phase 1A)

Generate a UUID per incoming share in the Share Extension, persist it
as sharedPhotoShareId in App Group metadata, and add [ShareTarget] logs
for receive/store/retrieve events without changing retrieval or deletion.
This commit is contained in:
Jose Olarte III
2026-06-23 19:13:44 +08:00
parent 08a55202f5
commit 35a6a6bfb3
3 changed files with 79 additions and 9 deletions

View File

@@ -12,6 +12,7 @@ The iOS share target uses a **Share Extension** (`TimeSafariShareExtension`) tha
|-----|---------|------------|---------|
| `sharedPhotoFilePath` | UserDefaults (suite) | Share Extension | Relative filename of image file in container |
| `sharedPhotoFileName` | UserDefaults (suite) | Share Extension | Display/original filename |
| `sharedPhotoShareId` | UserDefaults (suite) | Share Extension | Unique UUID per incoming share (Phase 1A) |
| `sharedPhotoReady` | UserDefaults (suite) | Share Extension | Boolean signal that a new share is available |
| `sharedPhotoBase64` | UserDefaults (suite) | *(legacy, not written)* | Removed on write for cleanup |
| Image file | App Group filesystem | Share Extension | Raw image bytes at `{container}/{sharedPhotoFilePath}` |
@@ -267,6 +268,62 @@ After native read, image data lives in SQLite `temp` table under key `shared-pho
---
## Share ID Tracking
**Implemented:** 2026-06-23 (Phase 1A)
Phase 1A adds a unique share identifier to the iOS share flow for observability and future reliability work. Existing retrieval and deletion behavior is unchanged.
### Identifier
| Property | Value |
|----------|-------|
| UserDefaults key | `sharedPhotoShareId` |
| Format | `UUID().uuidString` (e.g. `A1B2C3D4-E5F6-7890-ABCD-EF1234567890`) |
| Generated in | `ShareViewController.processSharedImage` when the first image attachment is found |
| Persisted in | `ShareViewController.storeImageData` alongside `sharedPhotoFilePath` and `sharedPhotoFileName` |
### Logging
All log lines use the prefix `[ShareTarget]` and include `shareId=<id>`:
| Event | File | Method | When |
|-------|------|--------|------|
| share received | `ShareViewController.swift` | `processSharedImage` | UUID generated before `loadItem` |
| file stored | `ShareViewController.swift` | `storeImageData` | After successful `imageData.write(to:)` |
| metadata stored | `ShareViewController.swift` | `storeImageData` | After UserDefaults `synchronize()` |
| share retrieved | `SharedImageUtility.swift` | `getSharedImageData` | After successful file read (only if `sharedPhotoShareId` is present) |
Example log sequence for a single share:
```
[ShareTarget] share received shareId=A1B2C3D4-E5F6-7890-ABCD-EF1234567890
[ShareTarget] file stored shareId=A1B2C3D4-E5F6-7890-ABCD-EF1234567890
[ShareTarget] metadata stored shareId=A1B2C3D4-E5F6-7890-ABCD-EF1234567890
[ShareTarget] share retrieved shareId=A1B2C3D4-E5F6-7890-ABCD-EF1234567890
```
### Phase 1A Scope (Intentionally Unchanged)
- `getSharedImageData()` still returns only `base64` and `fileName` to JavaScript
- `sharedPhotoShareId` is **not** deleted on retrieve (deletion deferred to a later phase)
- `hasSharedImage()`, `isSharedPhotoReady()`, and JS consumption paths are unchanged
- Android code is unchanged
### Write Inventory Addition
| File | Method | Key Written |
|------|--------|-------------|
| `ShareViewController.swift` | `storeImageData(_:fileName:shareId:)` | `sharedPhotoShareId` |
### Read Inventory Addition
| File | Method | Key Read |
|------|--------|----------|
| `SharedImageUtility.swift` | `getSharedImageData()` | `sharedPhotoShareId` (logging only) |
---
## Configuration References
| Resource | Value |

View File

@@ -13,6 +13,7 @@ public class SharedImageUtility {
private static let appGroupIdentifier = "group.app.timesafari.share"
private static let sharedPhotoFileNameKey = "sharedPhotoFileName"
private static let sharedPhotoFilePathKey = "sharedPhotoFilePath"
private static let sharedPhotoShareIdKey = "sharedPhotoShareId"
private static let sharedPhotoReadyKey = "sharedPhotoReady"
/// Get the App Group container URL for accessing shared files
@@ -39,13 +40,18 @@ public class SharedImageUtility {
}
let fileName = userDefaults.string(forKey: sharedPhotoFileNameKey) ?? "shared-image.jpg"
let shareId = userDefaults.string(forKey: sharedPhotoShareIdKey)
let fileURL = containerURL.appendingPathComponent(filePath)
// Read image data from file
guard let imageData = try? Data(contentsOf: fileURL) else {
return nil
}
if let shareId = shareId {
print("[ShareTarget] share retrieved shareId=\(shareId)")
}
// Convert file data to base64 for JavaScript consumption
let base64String = imageData.base64EncodedString()

View File

@@ -13,6 +13,7 @@ class ShareViewController: UIViewController {
private let appGroupIdentifier = "group.app.timesafari.share"
private let sharedPhotoFileNameKey = "sharedPhotoFileName"
private let sharedPhotoFilePathKey = "sharedPhotoFilePath"
private let sharedPhotoShareIdKey = "sharedPhotoShareId"
private let sharedImageFileName = "shared-image"
/// Get the App Group container URL for storing shared files
@@ -76,6 +77,9 @@ class ShareViewController: UIViewController {
continue
}
let shareId = UUID().uuidString
print("[ShareTarget] share received shareId=\(shareId)")
// Try to load raw data first to preserve original format
// This preserves the original image format without conversion
attachment.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { [weak self] (data, error) in
@@ -124,7 +128,7 @@ class ShareViewController: UIViewController {
}
// Store image as file in App Group container
if self.storeImageData(finalImageData, fileName: fileName) {
if self.storeImageData(finalImageData, fileName: fileName, shareId: shareId) {
completion(true)
} else {
completion(false)
@@ -149,7 +153,7 @@ class ShareViewController: UIViewController {
/// Store image data as a file in the App Group container
/// All images are stored as files regardless of size for consistency and simplicity
/// Returns true if successful, false otherwise
private func storeImageData(_ imageData: Data, fileName: String) -> Bool {
private func storeImageData(_ imageData: Data, fileName: String, shareId: String) -> Bool {
guard let containerURL = appGroupContainerURL else {
return false
}
@@ -168,20 +172,23 @@ class ShareViewController: UIViewController {
} catch {
return false
}
print("[ShareTarget] file stored shareId=\(shareId)")
// Store file path and filename in UserDefaults (small data, safe to store)
guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else {
return false
}
// Store relative path and filename
// Store relative path, filename, and share identifier
userDefaults.set(actualFileName, forKey: sharedPhotoFilePathKey)
userDefaults.set(fileName, forKey: sharedPhotoFileNameKey)
userDefaults.set(shareId, forKey: sharedPhotoShareIdKey)
// Clean up any old base64 data that might exist
userDefaults.removeObject(forKey: "sharedPhotoBase64")
userDefaults.synchronize()
print("[ShareTarget] metadata stored shareId=\(shareId)")
return true
}