Eliminate the iOS startup race between asynchronous SharedImage plugin registration and the first shared-image check. Previously the initial check fired on a fixed 1000ms timer that only assumed the native plugin (registered by AppDelegate with retries from T+500ms) was ready, so a slow registration could make the first getSharedImage() call throw and miss a cold-start share. Replace the fixed delay with waitForSharedImagePluginReady(), which probes the plugin via a read-only hasSharedImage() call and retries within a bounded budget (10 attempts, 300ms apart) until the plugin actually responds. The initial check runs only once readiness is confirmed, with a best-effort fallback if the budget is exhausted. Scope is limited to the initial startup check on iOS. appStateChange, appUrlOpen/handleDeepLink, router navigation, share processing, Android behavior, and all native Swift code are unchanged. Temporary share-target diagnostics are preserved and extended with startup readiness logging. Document the change as a Phase 2A section in doc/share-target-ios-audit.md.
28 KiB
iOS Share Target Implementation Audit
Generated: 2026-06-23 17:07:21 PST
Overview
The iOS share target uses a Share Extension (TimeSafariShareExtension) that writes a shared image to an App Group container (group.app.timesafari.share), then opens the main app via timesafari://. The main app reads the image through a native Capacitor plugin (SharedImagePlugin) and stores it in the JS temp database before routing to /shared-photo.
App Group Storage Model
| Key | Storage | Written by | Purpose |
|---|---|---|---|
sharedPhotoFilePath |
UserDefaults (suite) | Share Extension | On-disk filename in container (<shareId>.<ext>) |
sharedPhotoFileName |
UserDefaults (suite) | Share Extension | Original display filename from source app |
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} (<shareId>.<ext>) |
End-to-End Flow
External App (Photos, Safari, etc.)
│
▼
┌─────────────────────────────────────┐
│ TimeSafariShareExtension │
│ ShareViewController │
│ 1. viewDidLoad → processAndOpenApp │
│ 2. processSharedImage (async) │
│ 3. storeImageData → file + metadata │
│ 4. setSharedPhotoReadyFlag │
│ 5. openMainApp (timesafari://) │
│ 6. completeRequest │
└─────────────────────────────────────┘
│
▼ App Group: group.app.timesafari.share
│ (UserDefaults keys + image file)
│
▼
┌─────────────────────────────────────┐
│ Main App (app.timesafari) │
│ │
│ Native detection: │
│ • AppDelegate.applicationDidBecome │
│ Active → checkForSharedImageOn │
│ Activation (flag only) │
│ • AppDelegate.application(open:) │
│ → Capacitor URL handling │
│ │
│ JS detection (main.capacitor.ts): │
│ • setTimeout 1000ms startup check │
│ • appStateChange (isActive) │
│ • appUrlOpen → timesafari:// │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ SharedImagePlugin.getSharedImage() │
│ → SharedImageUtility │
│ .getSharedImageData() │
│ (read-only; leaves native data intact) │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ main.capacitor.ts │
│ storeSharedImageInTempDB() │
│ → SQLite temp table │
│ → router.push/replace /shared-photo│
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ SharedPhotoView.vue │
│ loadSharedImage() │
│ → reads temp DB, deletes temp row │
│ → displays image, user action │
└─────────────────────────────────────┘
Component Responsibilities
Share Extension (Writer)
| File | Method | Responsibility |
|---|---|---|
ios/App/TimeSafariShareExtension/ShareViewController.swift |
viewDidLoad() |
Entry point; triggers share processing on load |
processAndOpenApp() |
Orchestrates image extraction, flag set, app open, extension completion | |
processSharedImage(from:completion:) |
Iterates NSExtensionItem attachments; loads first UTType.image via loadItem |
|
storeImageData(_:fileName:) |
Writes image file to App Group container; writes metadata to UserDefaults | |
setSharedPhotoReadyFlag() |
Sets sharedPhotoReady = true in App Group UserDefaults |
|
openMainApp() |
Opens timesafari:// via responder chain or extensionContext.open |
|
getFileNameWithExtension(_:newExtension:) |
Helper for PNG fallback filename | |
ios/App/TimeSafariShareExtension/Info.plist |
— | Declares share-services extension; accepts 1 image max |
ios/App/TimeSafariShareExtension/TimeSafariShareExtension.entitlements |
— | Grants App Group group.app.timesafari.share |
Main App Native Layer (Reader)
| File | Method | Responsibility |
|---|---|---|
ios/App/App/SharedImageUtility.swift |
getSharedImageData() |
Read-only: reads file from App Group, returns base64 + fileName; leaves metadata and file intact (Phase 1C) |
hasSharedImage() |
Non-destructive existence check (metadata + file on disk) | |
isSharedPhotoReady() |
Reads sharedPhotoReady flag |
|
clearSharedPhotoReadyFlag() |
Removes sharedPhotoReady key |
|
ios/App/App/SharedImagePlugin.swift |
getSharedImage(_:) |
Capacitor bridge to getSharedImageData() |
hasSharedImage(_:) |
Capacitor bridge to hasSharedImage() |
|
ios/App/App/AppDelegate.swift |
application(_:didFinishLaunchingWithOptions:) |
Registers SharedImagePlugin with retry loop |
registerSharedImagePlugin() |
Manually registers plugin instance on Capacitor bridge | |
applicationDidBecomeActive(_:) |
Calls checkForSharedImageOnActivation() |
|
checkForSharedImageOnActivation() |
Checks ready flag, clears it, posts SharedPhotoReady NSNotification |
|
application(_:open:options:) |
Forwards URL opens (including timesafari://) to Capacitor |
|
ios/App/App/App.entitlements |
— | Grants App Group group.app.timesafari.share |
JavaScript Layer (Consumer)
| File | Method | Responsibility |
|---|---|---|
src/plugins/SharedImagePlugin.ts |
— | Registers Capacitor plugin name SharedImage |
src/plugins/definitions.ts |
— | TypeScript interface for getSharedImage / hasSharedImage |
src/main.capacitor.ts |
checkAndStoreNativeSharedImage() |
Calls SharedImage.getSharedImage(), stores in temp DB; guarded by isProcessingSharedImage lock |
storeSharedImageInTempDB() |
Clears old temp row, inserts base64 data URL into SQLite temp table |
|
checkForSharedImageAndNavigate() |
Checks native share, navigates to /shared-photo on success |
|
handleDeepLink() |
Handles timesafari:// empty-path URLs from share extension on iOS |
|
registerDeepLinkListener() |
Registers Capacitor appUrlOpen listener |
|
src/views/SharedPhotoView.vue |
mounted() / onRouteQueryChange() |
Loads image from temp DB for display |
loadSharedImage() |
Reads SHARED_PHOTO_BASE64_KEY from temp DB; deletes temp row after load |
|
src/router/index.ts |
— | Defines /shared-photo route |
src/libs/util.ts |
SHARED_PHOTO_BASE64_KEY |
Temp DB key constant ("shared-photo-base64") |
Read / Write / Delete Inventory
Writes — Shared Image Metadata (App Group UserDefaults)
| File | Method | Keys Written |
|---|---|---|
ShareViewController.swift |
storeImageData(_:fileName:) |
sharedPhotoFilePath, sharedPhotoFileName |
ShareViewController.swift |
setSharedPhotoReadyFlag() |
sharedPhotoReady (= true) |
Writes — Shared Image Files (App Group Container)
| File | Method | Details |
|---|---|---|
ShareViewController.swift |
storeImageData(_:fileName:) |
imageData.write(to:) at {containerURL}/{actualFileName} |
Reads — Shared Image Metadata (App Group UserDefaults)
| File | Method | Keys Read |
|---|---|---|
SharedImageUtility.swift |
getSharedImageData() |
sharedPhotoFilePath, sharedPhotoFileName |
SharedImageUtility.swift |
hasSharedImage() |
sharedPhotoFilePath |
SharedImageUtility.swift |
isSharedPhotoReady() |
sharedPhotoReady |
AppDelegate.swift |
checkForSharedImageOnActivation() |
sharedPhotoReady (via isSharedPhotoReady()) |
Reads — Shared Image Files (App Group Container)
| File | Method | Details |
|---|---|---|
ShareViewController.swift |
processSharedImage (URL path) |
Reads source image via Data(contentsOf: url) from security-scoped URL |
SharedImageUtility.swift |
getSharedImageData() |
Data(contentsOf: fileURL) from App Group container |
SharedImageUtility.swift |
hasSharedImage() |
FileManager.fileExists(atPath:) only (no data read) |
Deletes — Shared Image Metadata (App Group UserDefaults)
| File | Method | Keys Removed |
|---|---|---|
ShareViewController.swift |
storeImageData(_:fileName:) |
sharedPhotoBase64 (legacy cleanup) |
SharedImageUtility.swift |
clearSharedPhotoReadyFlag() |
sharedPhotoReady |
AppDelegate.swift |
checkForSharedImageOnActivation() |
sharedPhotoReady (via clearSharedPhotoReadyFlag()) |
Removed in Phase 1C (previously in SharedImageUtility.getSharedImageData()):
| Keys / files | Mechanism |
|---|---|
sharedPhotoFilePath |
userDefaults.removeObject(forKey:) |
sharedPhotoFileName |
userDefaults.removeObject(forKey:) |
Image file at sharedPhotoFilePath |
FileManager.removeItem(at:) |
userDefaults.synchronize() after deletion |
Called after metadata/file removal |
Deletes — Shared Image Files (App Group Container)
| File | Method | Details |
|---|---|---|
ShareViewController.swift |
storeImageData(_:fileName:shareId:) |
Removes previous pending share file at prior sharedPhotoFilePath before write |
Removed in Phase 1C (previously in SharedImageUtility.getSharedImageData()):
| Operation | Mechanism |
|---|---|
| Delete image file after successful read | FileManager.removeItem(at: fileURL) |
Secondary Storage (Post-Native Consumption)
After native read, image data lives in SQLite temp table under key shared-photo-base64:
| File | Method | Operation |
|---|---|---|
main.capacitor.ts |
storeSharedImageInTempDB() |
DELETE old row, then INSERT OR REPLACE |
SharedPhotoView.vue |
loadSharedImage() |
READ then DELETE temp row |
Timing, Delays, Retries, and Polling
| Location | Mechanism | Values | Purpose |
|---|---|---|---|
AppDelegate.swift didFinishLaunching |
Plugin registration retry loop | Initial delay 0.5s; up to 5 attempts; backoff 0.5s × attempt | Ensure Capacitor bridge exists before registering SharedImagePlugin |
main.capacitor.ts startup |
setTimeout |
1000ms (iOS only) | Deferred check for shared image on cold launch |
main.capacitor.ts startup |
setTimeout |
2000ms | Deferred registration of appUrlOpen deep-link listener |
main.capacitor.ts startup |
setTimeout |
1000ms | Log app initialization status |
main.capacitor.ts |
CapacitorApp.addListener("appStateChange") |
On every isActive === true |
Re-check shared image when app foregrounds |
main.capacitor.ts |
isProcessingSharedImage flag |
Synchronous JS lock | Prevents concurrent checkAndStoreNativeSharedImage() calls |
main.capacitor.ts |
Comment at line 214 | References polling | Stale comment — checkAndStoreNativeSharedImage() does not poll or retry |
No native polling or retry exists for reading App Group data. hasSharedImage() is exposed but never called from application JS code.
Current Startup Detection Points
| # | Layer | Trigger | File | Method | Action |
|---|---|---|---|---|---|
| 1 | Native | App becomes active (cold start + resume) | AppDelegate.swift |
applicationDidBecomeActive → checkForSharedImageOnActivation |
Reads sharedPhotoReady flag; clears flag; posts SharedPhotoReady NSNotification (no JS listener) |
| 2 | JS | Module load + 1000ms delay | main.capacitor.ts |
setTimeout → checkForSharedImageAndNavigate |
Calls SharedImage.getSharedImage(), stores in temp DB, navigates to /shared-photo |
| 3 | JS | App foreground | main.capacitor.ts |
appStateChange listener → checkForSharedImageAndNavigate |
Same as above |
| 4 | JS | Deep link timesafari:// |
main.capacitor.ts |
appUrlOpen → handleDeepLink → checkAndStoreNativeSharedImage |
iOS-only empty-path URL handling; navigates to /shared-photo |
| 5 | Native | URL open | AppDelegate.swift |
application(_:open:options:) |
Forwards to Capacitor ApplicationDelegateProxy (enables #4) |
| 6 | Native | Cold launch plugin setup | AppDelegate.swift |
didFinishLaunching → tryRegister |
Registers SharedImagePlugin (not a share check, but required for JS reads) |
Note: Detection points 1–4 can all fire for a single share event. Only the JS paths (#2–4) actually read and consume the image.
Current Deletion Points
App Group (Native)
| When | File | Method | What is deleted |
|---|---|---|---|
| Before new share write | ShareViewController.swift |
storeImageData |
Previous pending share file at prior sharedPhotoFilePath |
| Legacy cleanup on write | ShareViewController.swift |
storeImageData |
sharedPhotoBase64 UserDefaults key |
| On app activation (flag only) | AppDelegate.swift |
checkForSharedImageOnActivation |
sharedPhotoReady flag |
Removed in Phase 1C (no longer deleted on retrieve):
| When | File | Method | What was deleted |
|---|---|---|---|
| On successful read | SharedImageUtility.swift |
getSharedImageData |
sharedPhotoFilePath, sharedPhotoFileName, image file |
Temp Database (JS)
| When | File | Method | What is deleted |
|---|---|---|---|
| Before storing new share | main.capacitor.ts |
storeSharedImageInTempDB |
Prior shared-photo-base64 temp row |
| After view loads image | SharedPhotoView.vue |
loadSharedImage |
shared-photo-base64 temp row |
Important: Native image file and metadata persist after getSharedImageData() (Phase 1C). Cleanup is deferred to a later phase. The sharedPhotoReady flag is still cleared independently by AppDelegate on activation.
Potential Race Conditions
-
Multiple JS detection paths, repeatable native read.
applicationDidBecomeActive, the 1000ms startup timer,appStateChange, andappUrlOpencan all invokecheckAndStoreNativeSharedImage()close together. Since Phase 1C,getSharedImageData()is read-only and returns the same data on every call until a new share overwrites metadata or explicit cleanup is added. TheisProcessingSharedImageJS lock still reduces duplicate temp-DB writes and navigations. -
Deep-link listener registered 2 seconds after mount. The share extension opens
timesafari://immediately. If Capacitor does not buffer the launch URL until theappUrlOpenlistener is registered (at T+2000ms), the deep-link path may be missed on cold start. The 1000ms startup check andappStateChangepaths partially compensate. -
Plugin registration vs. first
getSharedImage()call.SharedImagePluginis registered with up to 5 retries starting at T+500ms. AgetSharedImage()call before registration completes will fail. The 1000ms startup delay usually avoids this, butappStateChangecan fire earlier. -
sharedPhotoReadyflag cleared before JS reads image.AppDelegate.checkForSharedImageOnActivationclears the flag and postsSharedPhotoReady, but no JavaScript code listens for that NSNotification. The flag is therefore a redundant signal; reliance is entirely on file/metadata presence. If file write failed but flag were set, the flag would be cleared with no image available (current code sets flag only after successfulstoreImageData). -
SharedPhotoReadyNSNotification is a dead signal. Posted inAppDelegatebut not bridged to Capacitor/JS. All actual consumption happens through JS-initiatedgetSharedImage()calls. -
Concurrent share while app is open. A second share overwrites the App Group file and metadata. If the first share has already been read into temp DB but the user has not yet reached
SharedPhotoView, the second share can replace native data; navigation refresh via_refreshquery param handles re-navigation but temp DB overwrite instoreSharedImageInTempDBcan clobber an in-flight first image. -
Extension
completeRequesttiming.completeRequestruns in theprocessSharedImagecompletion handler afterstoreImageData, flag set, andopenMainApp— so the file should exist before the extension exits. However,loadItemis asynchronous; if the extension process is terminated aggressively by iOS aftercompleteRequest, this is generally safe because all writes complete in the callback before completion. -
Stale comment implies polling that does not exist.
handleDeepLinkcomments reference internal polling incheckAndStoreNativeSharedImage, but no retry loop exists. A single failed read at the wrong moment is not retried on iOS (unlike Android's multi-delay startup checks). -
hasSharedImage()unused. A non-destructive pre-check is available natively but JS always callsgetSharedImage()directly. Since Phase 1C both methods are non-destructive on the native layer.
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 (Phase 1C log format) |
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 originalFilename=vacation.jpg storedFilename=A1B2C3D4-E5F6-7890-ABCD-EF1234567890.jpg
[ShareTarget] metadata stored shareId=A1B2C3D4-E5F6-7890-ABCD-EF1234567890 originalFilename=vacation.jpg storedFilename=A1B2C3D4-E5F6-7890-ABCD-EF1234567890.jpg
[ShareTarget] shareId=A1B2C3D4-E5F6-7890-ABCD-EF1234567890 retrieved
[ShareTarget] shareId=A1B2C3D4-E5F6-7890-ABCD-EF1234567890 left intact after retrieval
Phase 1A Scope (Intentionally Unchanged)
getSharedImageData()still returns onlybase64andfileNameto JavaScriptsharedPhotoShareIdis not deleted on retrieve (cleanup 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) |
Unique Stored Filenames
Implemented: 2026-06-23 (Phase 1B)
Phase 1B eliminates on-disk filename collisions by storing each shared image under a UUID-based filename while preserving the original filename as metadata for consumers.
On-Disk vs Metadata
| Field | UserDefaults key | Example | Purpose |
|---|---|---|---|
| Stored filename | sharedPhotoFilePath |
A1B2C3D4-E5F6-7890-ABCD-EF1234567890.jpg |
Unique file in App Group container |
| Original filename | sharedPhotoFileName |
vacation-photo.jpg |
Returned to JS as fileName |
| Share ID | sharedPhotoShareId |
A1B2C3D4-E5F6-7890-ABCD-EF1234567890 |
Correlates logs across extension and main app |
Stored filename format: <shareId>.<extension>, where extension is taken from the original filename (defaults to jpg when absent).
Implementation
| File | Method | Change |
|---|---|---|
ShareViewController.swift |
fileExtension(from:) |
Extracts extension from original filename |
ShareViewController.swift |
storedFileName(shareId:originalFileName:) |
Builds <shareId>.<ext> |
ShareViewController.swift |
storeImageData |
Writes to stored filename; saves original in sharedPhotoFileName |
SharedImageUtility.swift |
getSharedImageData |
Reads file via sharedPhotoFilePath; returns original sharedPhotoFileName |
When a new share arrives before the previous one is retrieved, storeImageData removes the file at the previous sharedPhotoFilePath before writing, preserving single-pending-share semantics.
Logging (Phase 1B)
Store and retrieve events include all three identifiers:
[ShareTarget] file stored shareId=<id> originalFilename=<name> storedFilename=<shareId>.<ext>
[ShareTarget] metadata stored shareId=<id> originalFilename=<name> storedFilename=<shareId>.<ext>
[ShareTarget] shareId=<id> retrieved
[ShareTarget] shareId=<id> left intact after retrieval
Phase 1B Scope (Intentionally Unchanged)
getSharedImageData()still returns onlybase64and originalfileNameto JavaScript- Android code is unchanged
Non-Destructive Retrieval
Implemented: 2026-06-24 (Phase 1C)
Phase 1C makes native shared-content retrieval read-only. getSharedImageData() and SharedImagePlugin.getSharedImage() no longer delete App Group metadata or image files after a successful read. Explicit cleanup is deferred to a later phase.
Behavior Change
| Aspect | Before Phase 1C | After Phase 1C |
|---|---|---|
sharedPhotoFilePath after retrieve |
Removed | Retained |
sharedPhotoFileName after retrieve |
Removed | Retained |
sharedPhotoShareId after retrieve |
Retained (since Phase 1A) | Retained |
| Image file after retrieve | Deleted | Retained |
| Return value to JS | { base64, fileName } |
Unchanged |
Repeat getSharedImage() calls |
Return null after first success |
Return same data until overwritten or cleaned up |
Logging
After a successful read:
[ShareTarget] shareId=<id> retrieved
[ShareTarget] shareId=<id> left intact after retrieval
Removed Deletion Paths
All removal logic was in SharedImageUtility.getSharedImageData():
| # | What was deleted | Code removed |
|---|---|---|
| 1 | sharedPhotoFilePath UserDefaults key |
userDefaults.removeObject(forKey: sharedPhotoFilePathKey) |
| 2 | sharedPhotoFileName UserDefaults key |
userDefaults.removeObject(forKey: sharedPhotoFileNameKey) |
| 3 | Image file at {container}/{sharedPhotoFilePath} |
FileManager.default.removeItem(at: fileURL) |
| 4 | Post-deletion UserDefaults flush | userDefaults.synchronize() after removals |
SharedImagePlugin.getSharedImage(_:) delegated to getSharedImageData() and had no independent deletion logic. Comment updated to reflect read-only behavior.
Phase 1C Scope (Intentionally Unchanged)
- No new cleanup or purge APIs added
clearSharedPhotoReadyFlag()and share-extension write-side file removal unchanged- JS temp DB deletion in
main.capacitor.tsandSharedPhotoView.vueunchanged - Android code unchanged
Deterministic Startup Plugin Readiness
Implemented: 2026-06-26 (Phase 2A)
Phase 2A removes the iOS startup race between native SharedImage plugin
registration and the first JS shared-image check. All changes are confined to
src/main.capacitor.ts; no Swift code changed.
Race condition removed
Previously the initial iOS shared-image check ran on a fixed timer:
const checkDelays = ... : [1000]; // iOS
checkDelays.forEach((delay) => setTimeout(() => checkForSharedImageAndNavigate(), delay));
The native SharedImagePlugin is registered asynchronously by
AppDelegate.didFinishLaunchingWithOptions with up to 5 retries starting at
T+500ms (AppDelegate.swift:21–40). The fixed 1000ms JS delay only assumed
registration had completed by then. When registration was slow (or the WebView
booted unusually fast), the first checkForSharedImageAndNavigate() could call
SharedImage.getSharedImage() before the native plugin existed, the call would
throw, and the cold-start share could be missed until a later appStateChange.
This corresponds to race condition #3 in Potential Race Conditions above.
How plugin readiness is now determined
The fixed 1000ms iOS delay is replaced with an explicit, deterministic wait
(waitForSharedImagePluginReady() in main.capacitor.ts):
- The plugin is probed with a lightweight, read-only
SharedImage.hasSharedImage()call. A successful resolution proves the native plugin instance is registered and reachable from JS.hasSharedImage()does not consume or mutate the pending share (non-destructive since Phase 1C), so probing is side-effect free. - If the probe throws (plugin not yet registered), it retries within a bounded
budget:
STARTUP_PLUGIN_MAX_ATTEMPTS = 10attempts spacedSTARTUP_PLUGIN_RETRY_DELAY_MS = 300ms apart (~3s ceiling, covering the native registration window). No arbitrary sleep is used to assume readiness; the delay is only the inter-retry backoff while polling for actual availability. - The very first
checkForSharedImageAndNavigate()runs only after the probe succeeds. If the budget is exhausted (should not happen in practice), the check is still attempted once as a best-effort fallback so behavior is never worse than the previous fixed-delay path, andappStateChangeretries on the next activation.
Temporary diagnostics
The retry sequence emits [ShareTarget] console diagnostics, consistent with the
existing TEMPORARY SHARE TARGET DIAGNOSTICS convention:
[ShareTarget] Startup shared-image check waiting for SharedImage plugin
[ShareTarget] SharedImage plugin ready after N attempt(s)
[ShareTarget] Startup shared-image check giving up after N attempt(s)
Phase 2A Scope (Intentionally Unchanged)
The retry/readiness logic applies only to the initial startup shared-image check. The following are deliberately untouched:
appStateChangehandling (CapacitorApp.addListener("appStateChange", ...))appUrlOpenhandling (handleDeepLink,registerDeepLinkListener)- Router navigation to
/shared-photo - Share processing (
checkAndStoreNativeSharedImage,storeSharedImageInTempDB) - Android startup behavior (still
[500, 1500, 3000]ms multi-delay checks) - All native Swift code, including the
AppDelegateplugin-registration retry
Because readiness is now confirmed by an actual plugin response rather than a timer, the startup check no longer depends on registration timing, while every other detection path keeps its previous semantics as redundant backstops.
Configuration References
| Resource | Value |
|---|---|
| App Group ID | group.app.timesafari.share |
| URL scheme | timesafari:// |
| Extension bundle ID | app.timesafari.TimeSafariShareExtension |
| Main app bundle ID | app.timesafari |
| Capacitor plugin name | SharedImage |
| Temp DB key | shared-photo-base64 (SHARED_PHOTO_BASE64_KEY) |
| Route | /shared-photo |