From 35a6a6bfb3771818ce4d78999162694b263ae4a3 Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Tue, 23 Jun 2026 19:13:44 +0800 Subject: [PATCH] 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. --- doc/share-target-ios-audit.md | 57 +++++++++++++++++++ ios/App/App/SharedImageUtility.swift | 10 +++- .../ShareViewController.swift | 21 ++++--- 3 files changed, 79 insertions(+), 9 deletions(-) diff --git a/doc/share-target-ios-audit.md b/doc/share-target-ios-audit.md index 1cb88afa..de724d60 100644 --- a/doc/share-target-ios-audit.md +++ b/doc/share-target-ios-audit.md @@ -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=`: + +| 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 | diff --git a/ios/App/App/SharedImageUtility.swift b/ios/App/App/SharedImageUtility.swift index 08716d63..1c524cba 100644 --- a/ios/App/App/SharedImageUtility.swift +++ b/ios/App/App/SharedImageUtility.swift @@ -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() diff --git a/ios/App/TimeSafariShareExtension/ShareViewController.swift b/ios/App/TimeSafariShareExtension/ShareViewController.swift index cf967dff..4e27880f 100644 --- a/ios/App/TimeSafariShareExtension/ShareViewController.swift +++ b/ios/App/TimeSafariShareExtension/ShareViewController.swift @@ -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 }