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:
@@ -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 |
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user