diff --git a/doc/share-extension-app-group-audit.md b/doc/share-extension-app-group-audit.md
deleted file mode 100644
index fe7518fe..00000000
--- a/doc/share-extension-app-group-audit.md
+++ /dev/null
@@ -1,155 +0,0 @@
-# iOS App Group Configuration Audit
-
-**Generated:** 2026-06-25 17:31:15 PST
-
-## Scope
-
-Static inspection of App Group configuration for the **App** target and the **TimeSafariShareExtension** target: entitlements, capabilities, bundle identifiers, Debug/Release build settings, and signing. No code was modified.
-
-### Files Inspected
-
-| File | Role |
-|------|------|
-| `ios/App/App/App.entitlements` | App target App Group declaration |
-| `ios/App/TimeSafariShareExtension/TimeSafariShareExtension.entitlements` | Extension App Group declaration |
-| `ios/App/App.xcodeproj/project.pbxproj` | Bundle IDs, teams, signing, entitlement linkage |
-| `ios/App/App/SharedImageUtility.swift` | App Group identifier used by main app code |
-| `ios/App/TimeSafariShareExtension/ShareViewController.swift` | App Group identifier used by extension code |
-
----
-
-## CRITICAL FINDING — Code vs Entitlements App Group Mismatch
-
-The entitlements and the Swift source declare **different** App Group identifiers:
-
-| Location | App Group identifier |
-|----------|----------------------|
-| `App.entitlements` | `group.app.trentlarson.timesafari.share` |
-| `TimeSafariShareExtension.entitlements` | `group.app.trentlarson.timesafari.share` |
-| `SharedImageUtility.swift` (`appGroupIdentifier`) | `group.app.timesafari.share` |
-| `ShareViewController.swift` (`appGroupIdentifier`) | `group.app.timesafari.share` |
-
-The runtime code targets `group.app.timesafari.share`, but **neither target is entitled to that group** — both entitlements now grant `group.app.trentlarson.timesafari.share`.
-
-This is an **uncommitted change**: `git diff` shows both entitlements were just changed from `group.app.timesafari.share` → `group.app.trentlarson.timesafari.share`, while the Swift code still uses the old value. Before this edit the code and entitlements matched; after it they do not.
-
-### Runtime Consequences
-
-- `FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.app.timesafari.share")` returns **nil** (the app is not entitled to that group). The extension's `storeImageData` aborts via `guard let containerURL` → image file is never written; the main app's reads return nil.
-- `UserDefaults(suiteName: "group.app.timesafari.share")` does **not** resolve to the shared, entitled suite. Writes fall back to each process's own preferences domain, so the extension's keys (`sharedPhotoFilePath`, `sharedPhotoShareId`, `shareExtensionLastStart`, `sharedPhotoReady`) are **not visible** to the main app.
-
-Net effect: the entire share-target handoff via the App Group breaks while this mismatch exists. This is the most likely root cause of "App Group UserDefaults writes failing."
-
-**Note:** This affects both Debug and Release (the entitlements have no per-configuration variants), not Debug only.
-
----
-
-## Direct Answers
-
-### Do both targets declare the same App Group?
-
-**Yes — the two entitlements files match each other.** Both `App.entitlements` and `TimeSafariShareExtension.entitlements` declare exactly:
-
-```xml
-com.apple.security.application-groups
-
- group.app.trentlarson.timesafari.share
-
-```
-
-However, **the code does not match the entitlements** (see Critical Finding). So "same App Group" is true at the entitlement level, false at the entitlement-vs-code level.
-
-### Are there any Debug vs Release differences?
-
-**Entitlements / App Group:** No. A single entitlements file per target applies to both configurations; the App Group string is identical in Debug and Release (`group.app.trentlarson.timesafari.share`).
-
-**Bundle identifiers:** Yes — they differ by configuration:
-
-| Target | Debug | Release |
-|--------|-------|---------|
-| App | `app.trentlarson.timesafari` | `app.timesafari` |
-| Extension | `app.trentlarson.timesafari.TimeSafariShareExtension` | `app.timesafari.TimeSafariShareExtension` |
-
-(The Debug bundle IDs were just changed from the `app.timesafari*` form per `git diff`.)
-
-**Development team:** Yes — differs by configuration (see next answer).
-
-In both configurations the extension bundle ID is correctly nested under the app bundle ID, which is required for an app extension.
-
-### Are there any team-ID differences that could affect App Group access?
-
-| Configuration | App team | Extension team | Match? |
-|---------------|----------|----------------|--------|
-| Debug | `7XVXYPEQYJ` | `7XVXYPEQYJ` | ✅ same |
-| Release | `GM3FS5JQPH` | `GM3FS5JQPH` | ✅ same |
-
-- **Within each configuration, both targets use the same team** — this is the condition required for two targets to share an App Group, and it is satisfied.
-- **Across configurations the teams differ** (Debug `7XVXYPEQYJ` vs Release `GM3FS5JQPH`). The Debug team was just changed from `GM3FS5JQPH` per `git diff`.
-
-Implications:
-1. The App Group container is namespaced by Team ID at runtime (`$(TeamID).group...`). A Debug install (team `7XVXYPEQYJ`) and a Release install (team `GM3FS5JQPH`) use **different physical containers** and cannot share data with each other. This is normal and only matters if you expect data continuity between Debug and Release builds.
-2. With **Automatic** signing, the App Group `group.app.trentlarson.timesafari.share` must be registered/enabled for **both** teams. If it is not provisioned under the Debug team `7XVXYPEQYJ`, automatic signing of the Debug build can fail to include the App Group entitlement (or fail to sign), which would also break App Group access in Debug.
-
-### Are there signing/entitlement mismatches that could cause App Group UserDefaults writes to fail in Debug builds?
-
-**Yes.** In order of severity:
-
-1. **(Primary) Code/entitlement group-ID mismatch.** Code uses `group.app.timesafari.share`; entitlements grant `group.app.trentlarson.timesafari.share`. The code's group is not entitled, so shared `UserDefaults`/container access fails. Affects Debug and Release.
-
-2. **(Debug-specific risk) App Group provisioning under the Debug team.** Debug now signs with team `7XVXYPEQYJ` (changed from `GM3FS5JQPH`). Under Automatic signing, if `group.app.trentlarson.timesafari.share` is not enabled for team `7XVXYPEQYJ`, the Debug build's App Group entitlement may not be granted, causing writes to silently fall back to the local domain.
-
-3. **(Consistency) Bundle-ID change accompanying the team change.** Debug bundle IDs changed to `app.trentlarson.timesafari*`. App Groups don't have to match bundle IDs, so this is not a direct cause, but combined with the new team it means Debug provisioning is a distinct profile/identifier set that must independently carry the App Group capability.
-
-No mismatch was found **between the two entitlement files themselves**, and no per-configuration entitlement override exists.
-
----
-
-## Detailed Configuration
-
-### Entitlements (identical content in both files)
-
-```xml
-com.apple.security.application-groups
-
- group.app.trentlarson.timesafari.share
-
-```
-
-`CODE_SIGN_ENTITLEMENTS` linkage (both Debug and Release):
-
-| Target | Entitlements path |
-|--------|-------------------|
-| App | `App/App.entitlements` |
-| Extension | `TimeSafariShareExtension/TimeSafariShareExtension.entitlements` |
-
-### Bundle Identifiers, Teams, Signing (project.pbxproj)
-
-| Setting | App Debug | App Release | Ext Debug | Ext Release |
-|---------|-----------|-------------|-----------|-------------|
-| `PRODUCT_BUNDLE_IDENTIFIER` | `app.trentlarson.timesafari` | `app.timesafari` | `app.trentlarson.timesafari.TimeSafariShareExtension` | `app.timesafari.TimeSafariShareExtension` |
-| `DEVELOPMENT_TEAM` | `7XVXYPEQYJ` | `GM3FS5JQPH` | `7XVXYPEQYJ` | `GM3FS5JQPH` |
-| `CODE_SIGN_STYLE` | Automatic | Automatic | Automatic | Automatic |
-| `CODE_SIGN_ENTITLEMENTS` | `App/App.entitlements` | same | `TimeSafariShareExtension/...entitlements` | same |
-| App Group (from entitlements) | `group.app.trentlarson.timesafari.share` | same | same | same |
-
-### App Group Identifier Used in Code
-
-```swift
-// SharedImageUtility.swift:13 and ShareViewController.swift:13
-private let appGroupIdentifier = "group.app.timesafari.share" // ← does NOT match entitlements
-```
-
----
-
-## Recommendations (no code changed)
-
-1. **Resolve the group-ID mismatch.** Either revert the entitlements back to `group.app.timesafari.share`, or update the two Swift constants to `group.app.trentlarson.timesafari.share`. Both sides must use one identical string.
-2. **Confirm App Group provisioning per team.** Ensure `group.app.trentlarson.timesafari.share` (whichever string is chosen) is enabled for both `7XVXYPEQYJ` (Debug) and `GM3FS5JQPH` (Release) so Automatic signing includes the capability in both configurations.
-3. **Decide whether the Debug↔Release team/bundle-ID split is intentional.** If cross-config data continuity is ever expected, note that different Team IDs yield different App Group containers.
-4. **Verify at runtime** using the existing `getShareExtensionDiagnostics()` / `[ShareTarget]` logs: after aligning identifiers, `shareExtensionLastStart` written by the extension should become readable by the main app.
-
----
-
-## Conclusion
-
-The two **entitlement files agree** on the App Group (`group.app.trentlarson.timesafari.share`) and, **within each build configuration**, both targets share the same Development Team and consistent nested bundle IDs — the structural requirements for App Group sharing are met. The decisive problem is that the **Swift code still references the old group `group.app.timesafari.share`**, which no entitlement grants; this breaks both shared `UserDefaults` and the shared container in all builds. Secondarily, the recent Debug switch to team `7XVXYPEQYJ` means the chosen App Group must be provisioned under that team for Debug App Group access to work under Automatic signing.
diff --git a/doc/share-extension-configuration-audit.md b/doc/share-extension-configuration-audit.md
deleted file mode 100644
index be8526a6..00000000
--- a/doc/share-extension-configuration-audit.md
+++ /dev/null
@@ -1,188 +0,0 @@
-# iOS Share Extension Configuration Audit
-
-**Generated:** 2026-06-25 15:33:39 PST
-
-## Scope
-
-Static inspection of the `TimeSafariShareExtension` target configuration to determine the extension entry point, principal view controller, storyboard vs. code-based setup, and whether `ShareViewController.viewDidLoad()` is guaranteed to execute. No code was modified.
-
-### Files Inspected
-
-| File | Role |
-|------|------|
-| `ios/App/App.xcodeproj/project.pbxproj` | Target, build settings, file membership |
-| `ios/App/TimeSafariShareExtension/Info.plist` | NSExtension configuration |
-| `ios/App/TimeSafariShareExtension/ShareViewController.swift` | Principal class implementation |
-| `ios/App/TimeSafariShareExtension/TimeSafariShareExtension.entitlements` | App Group access |
-
----
-
-## Direct Answers
-
-### 1. What class is configured as the extension entry point?
-
-`ShareViewController`, resolved via `Info.plist` key `NSExtensionPrincipalClass`:
-
-```xml
-NSExtensionPrincipalClass
-$(PRODUCT_MODULE_NAME).ShareViewController
-```
-
-`$(PRODUCT_MODULE_NAME)` resolves to `TimeSafariShareExtension` (derived from `PRODUCT_NAME = $(TARGET_NAME)`), so the runtime entry point is `TimeSafariShareExtension.ShareViewController`.
-
-### 2. Is ShareViewController actually the configured principal view controller?
-
-**Yes.** `ShareViewController.swift` declares:
-
-```swift
-class ShareViewController: UIViewController {
-```
-
-within the `TimeSafariShareExtension` target. The class name, module, and `UIViewController` base class match the `NSExtensionPrincipalClass` reference. There is no competing storyboard-designated initial controller, so `ShareViewController` is unambiguously the principal view controller.
-
-### 3. Is the extension storyboard-based or code-based?
-
-**Code-based.**
-
-- `Info.plist` contains `NSExtensionPrincipalClass` and does **not** contain `NSExtensionMainStoryboard`.
-- The extension folder contains no `.storyboard` file (only `Info.plist`, `ShareViewController.swift`, and the entitlements file).
-- The only storyboards in the project (`Main.storyboard`, `LaunchScreen.storyboard`) belong exclusively to the **App** target's resources, not the extension.
-
-This deviates from the default Xcode Share Extension template (which ships a `MainInterface.storyboard` + `NSExtensionMainStoryboard`). The deviation is intentional and internally consistent.
-
-### 4. Does the configuration guarantee that ShareViewController.viewDidLoad() executes when the extension launches?
-
-**Yes, under normal launch.** Because:
-
-- The principal class is a `UIViewController` subclass, the extension host instantiates it and installs its view into the extension's window. This triggers the standard view lifecycle: `loadView()` → `viewDidLoad()`.
-- `ShareViewController` overrides `viewDidLoad()` and calls `super.viewDidLoad()`, then immediately runs `processAndOpenApp()`. The startup marker (`shareExtensionLastStart`) and `[ShareTarget] viewDidLoad started` log execute before any other logic.
-- The Swift source is compiled into the extension target via the Xcode 16 **file-system synchronized group** (`PBXFileSystemSynchronizedRootGroup` for `TimeSafariShareExtension`), so the class is guaranteed to be present in the built `.appex`.
-
-**Caveats (not failures, but worth noting):**
-- The guarantee holds only if the OS successfully resolves and instantiates the principal class. If `$(PRODUCT_MODULE_NAME)` ever diverges from the actual Swift module name (e.g., a custom `PRODUCT_MODULE_NAME`), runtime class lookup would fail and the extension would not launch. Currently they match.
-- `viewDidLoad()` executing does not, by itself, guarantee the *share* succeeds — the asynchronous `loadItem` work in `processSharedImage` happens after `viewDidLoad` returns.
-
-### 5. Are there any mismatches between Info.plist, storyboard, and ShareViewController?
-
-**No blocking mismatches.** Details:
-
-| Check | Result |
-|-------|--------|
-| `NSExtensionPrincipalClass` ↔ Swift class name | Match (`ShareViewController`) |
-| Principal class module ↔ target module | Match (`TimeSafariShareExtension`) |
-| `NSExtensionMainStoryboard` ↔ storyboard file | Consistent — neither exists (code-based) |
-| Activation rule ↔ implementation | Consistent — `NSExtensionActivationSupportsImageWithMaxCount = 1` matches first-image-only handling |
-| `NSExtensionPointIdentifier` | `com.apple.share-services` (correct for a Share extension) |
-| Source file membership | `ShareViewController.swift` compiled via synchronized group |
-
-See "Observations / Non-Blocking Notes" for environment-specific items.
-
----
-
-## Detailed Configuration
-
-### NSExtension (Info.plist)
-
-```xml
-NSExtension
-
- NSExtensionAttributes
-
- NSExtensionActivationRule
-
- NSExtensionActivationSupportsImageWithMaxCount
- 1
-
-
- NSExtensionPointIdentifier
- com.apple.share-services
- NSExtensionPrincipalClass
- $(PRODUCT_MODULE_NAME).ShareViewController
-
-```
-
-| Key | Value | Meaning |
-|-----|-------|---------|
-| `NSExtensionPointIdentifier` | `com.apple.share-services` | Registers as a Share sheet extension |
-| `NSExtensionPrincipalClass` | `$(PRODUCT_MODULE_NAME).ShareViewController` | Code-based entry point |
-| `NSExtensionActivationRule` | `…ImageWithMaxCount = 1` | Activates for shares containing at least one image; processes one |
-| `NSExtensionMainStoryboard` | *absent* | Confirms code-based (no storyboard UI) |
-
-### TimeSafariShareExtension Target (project.pbxproj)
-
-| Attribute | Value |
-|-----------|-------|
-| `isa` | `PBXNativeTarget` |
-| `productType` | `com.apple.product-type.app-extension` |
-| `productReference` | `TimeSafariShareExtension.appex` |
-| `CreatedOnToolsVersion` | `26.1.1` |
-| File membership | `fileSystemSynchronizedGroups` → `TimeSafariShareExtension` (auto-membership) |
-| Sources build phase | Empty explicit list (handled by synchronized group) |
-| `Info.plist` membership | Excepted from synchronized group (`PBXFileSystemSynchronizedBuildFileExceptionSet`) |
-| Embedded into | App target's "Embed Foundation Extensions" copy phase |
-| Target dependency | App target depends on `TimeSafariShareExtension` |
-
-### Extension Build Settings (Debug / Release)
-
-| Setting | Debug | Release |
-|---------|-------|---------|
-| `INFOPLIST_FILE` | `TimeSafariShareExtension/Info.plist` | same |
-| `GENERATE_INFOPLIST_FILE` | `YES` | `YES` |
-| `PRODUCT_NAME` | `$(TARGET_NAME)` → `TimeSafariShareExtension` | same |
-| `PRODUCT_BUNDLE_IDENTIFIER` | `app.trentlarson.timesafari.TimeSafariShareExtension` | `app.timesafari.TimeSafariShareExtension` |
-| `CODE_SIGN_ENTITLEMENTS` | `TimeSafariShareExtension/TimeSafariShareExtension.entitlements` | same |
-| `IPHONEOS_DEPLOYMENT_TARGET` | `14.0` | `14.0` |
-| `DEVELOPMENT_TEAM` | `7XVXYPEQYJ` | `GM3FS5JQPH` |
-| `SWIFT_VERSION` | `5.0` | `5.0` |
-| `SKIP_INSTALL` | `YES` | `YES` |
-
-`PRODUCT_MODULE_NAME` is not overridden, so it defaults to `PRODUCT_NAME` = `TimeSafariShareExtension`, making the principal class resolve to `TimeSafariShareExtension.ShareViewController`.
-
-### ShareViewController Linkage
-
-```swift
-import UIKit
-import UniformTypeIdentifiers
-
-class ShareViewController: UIViewController {
- ...
- override func viewDidLoad() {
- // writes shareExtensionLastStart, logs, then super + processAndOpenApp()
- }
-}
-```
-
-- Subclass of `UIViewController` → eligible as a code-based principal class.
-- Lives in the `TimeSafariShareExtension` target via the synchronized group.
-- No `@objc(...)` annotation is required because the principal class is referenced with the fully-qualified Swift name (`module.Class`).
-
-### Scene / Lifecycle Configuration
-
-- **No** `UIApplicationSceneManifest` / `UISceneConfigurations` in the extension `Info.plist`.
-- **No** `SceneDelegate` in the extension target.
-- The extension relies entirely on the principal `UIViewController` lifecycle (`viewDidLoad` → `processAndOpenApp` → `processSharedImage` → `completeRequest`).
-- The main app (`AppDelegate`) is a `UIApplicationDelegate` and is unrelated to the extension's lifecycle except via the shared App Group.
-
-### App Group Linkage
-
-`TimeSafariShareExtension.entitlements` grants `group.app.timesafari.share`, matching the App target's entitlement. This is what allows `viewDidLoad()`'s `shareExtensionLastStart` write to be visible to the main app's `getShareExtensionDiagnostics()`.
-
----
-
-## Observations / Non-Blocking Notes
-
-1. **Config-dependent bundle IDs & teams.** Debug uses `app.trentlarson.timesafari*` with team `7XVXYPEQYJ`; Release uses `app.timesafari*` with team `GM3FS5JQPH`. Within each configuration the extension bundle ID is correctly nested under the app bundle ID. Ensure provisioning profiles for both teams include the App Group capability.
-
-2. **`GENERATE_INFOPLIST_FILE = YES` alongside an explicit `INFOPLIST_FILE`.** Xcode merges auto-generated keys into the supplied `Info.plist`. This is supported and the explicit `NSExtension` block is preserved; no conflict observed.
-
-3. **Deployment target gap.** Extension targets iOS 14.0 while the App target targets iOS 15.5. Valid (an extension may target lower), and not a launch concern.
-
-4. **Principal-class resolution dependency.** The launch guarantee depends on `$(PRODUCT_MODULE_NAME)` matching the compiled module. If `PRODUCT_MODULE_NAME` is later customized or the target renamed without updating expectations, the OS would fail to instantiate `ShareViewController` and `viewDidLoad()` would never run. Currently consistent.
-
-5. **Code-based template divergence.** Since there is no `MainInterface.storyboard`, any future tooling or documentation that assumes the stock storyboard-based Share Extension template will not apply here.
-
----
-
-## Conclusion
-
-The `TimeSafariShareExtension` is a **code-based** Share extension whose entry point is `ShareViewController` (a `UIViewController` subclass) via `NSExtensionPrincipalClass`. The Info.plist, (absent) storyboard, and Swift implementation are mutually consistent. Under normal extension launch, `ShareViewController.viewDidLoad()` is guaranteed to run, executing the startup marker and the share-processing pipeline. No blocking misconfiguration was found; only environment-specific items (signing identities, principal-class resolution dependency) warrant ongoing attention.
diff --git a/doc/share-target-ios-audit.md b/doc/share-target-ios-audit.md
deleted file mode 100644
index 0a4c3a02..00000000
--- a/doc/share-target-ios-audit.md
+++ /dev/null
@@ -1,524 +0,0 @@
-# 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 (`.`) |
-| `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}` (`.`) |
-
----
-
-## 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
-
-1. **Multiple JS detection paths, repeatable native read.** `applicationDidBecomeActive`, the 1000ms startup timer, `appStateChange`, and `appUrlOpen` can all invoke `checkAndStoreNativeSharedImage()` 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. The `isProcessingSharedImage` JS lock still reduces duplicate temp-DB writes and navigations.
-
-2. **Deep-link listener registered 2 seconds after mount.** The share extension opens `timesafari://` immediately. If Capacitor does not buffer the launch URL until the `appUrlOpen` listener is registered (at T+2000ms), the deep-link path may be missed on cold start. The 1000ms startup check and `appStateChange` paths partially compensate.
-
-3. **Plugin registration vs. first `getSharedImage()` call.** `SharedImagePlugin` is registered with up to 5 retries starting at T+500ms. A `getSharedImage()` call before registration completes will fail. The 1000ms startup delay usually avoids this, but `appStateChange` can fire earlier.
-
-4. **`sharedPhotoReady` flag cleared before JS reads image.** `AppDelegate.checkForSharedImageOnActivation` clears the flag and posts `SharedPhotoReady`, 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 successful `storeImageData`).
-
-5. **`SharedPhotoReady` NSNotification is a dead signal.** Posted in `AppDelegate` but not bridged to Capacitor/JS. All actual consumption happens through JS-initiated `getSharedImage()` calls.
-
-6. **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 `_refresh` query param handles re-navigation but temp DB overwrite in `storeSharedImageInTempDB` can clobber an in-flight first image.
-
-7. **Extension `completeRequest` timing.** `completeRequest` runs in the `processSharedImage` completion handler after `storeImageData`, flag set, and `openMainApp` — so the file should exist before the extension exits. However, `loadItem` is asynchronous; if the extension process is terminated aggressively by iOS after `completeRequest`, this is generally safe because all writes complete in the callback before completion.
-
-8. **Stale comment implies polling that does not exist.** `handleDeepLink` comments reference internal polling in `checkAndStoreNativeSharedImage`, 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).
-
-9. **`hasSharedImage()` unused.** A non-destructive pre-check is available natively but JS always calls `getSharedImage()` 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=`:
-
-| 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 only `base64` and `fileName` to JavaScript
-- `sharedPhotoShareId` is 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: `.`, 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 `.` |
-| `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= originalFilename= storedFilename=.
-[ShareTarget] metadata stored shareId= originalFilename= storedFilename=.
-[ShareTarget] shareId= retrieved
-[ShareTarget] shareId= left intact after retrieval
-```
-
-### Phase 1B Scope (Intentionally Unchanged)
-
-- `getSharedImageData()` still returns only `base64` and original `fileName` to 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= retrieved
-[ShareTarget] shareId= 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.ts` and `SharedPhotoView.vue` unchanged
-- 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:
-
-```ts
-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 = 10` attempts spaced
- `STARTUP_PLUGIN_RETRY_DELAY_MS = 300` ms 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, and `appStateChange` retries 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:
-
-- `appStateChange` handling (`CapacitorApp.addListener("appStateChange", ...)`)
-- `appUrlOpen` handling (`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 `AppDelegate` plugin-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` |
diff --git a/doc/share-target-ios-launch-flow-audit.md b/doc/share-target-ios-launch-flow-audit.md
deleted file mode 100644
index f66b6522..00000000
--- a/doc/share-target-ios-launch-flow-audit.md
+++ /dev/null
@@ -1,515 +0,0 @@
-# iOS Share-Target Launch & Deep-Link Flow Audit
-
-**Date:** 2026-06-26 15:32:14 PST
-**Scope:** iOS Share Extension launch path and `timesafari://` deep-link handling only.
-**Status:** Read-only audit. No code was modified. **No code changes are recommended.**
-
-This document traces the complete execution path that begins when the iOS
-Share Extension calls `application.open("timesafari://")` and ends with
-navigation to `/shared-photo`. All references cite file, method, and
-approximate line numbers as of the audit date.
-
-### Files in scope
-
-| File | Role |
-| --- | --- |
-| `ios/App/TimeSafariShareExtension/ShareViewController.swift` | Share Extension: stores image to App Group, opens `timesafari://` |
-| `ios/App/App/AppDelegate.swift` | Native app delegate: lifecycle + URL open proxy |
-| `ios/App/App/SharedImagePlugin.swift` | Capacitor plugin bridge (`SharedImage`) |
-| `ios/App/App/SharedImageUtility.swift` | App Group read/write helpers + ready flag |
-| `src/main.capacitor.ts` | JS bootstrap, deep-link listener, shared-image checks, navigation |
-| `src/main.common.ts` | `initializeApp()` — Vue app + router construction |
-| `src/router/index.ts` | Router creation, `/shared-photo` route |
-| `src/libs/capacitor/app.ts` | Type-safe wrapper around `@capacitor/app` listeners |
-| `src/plugins/SharedImagePlugin.ts` | JS `registerPlugin("SharedImage")` |
-| `capacitor.config.ts` | `appUrlOpen` handler config for `timesafari://*` |
-
----
-
-## 1. Cold-start launch path
-
-"Cold start" = the main app process is **not** running when the user taps Share.
-
-### Execution order
-
-1. **Share Extension `viewDidLoad()`**
- `ShareViewController.swift:47` → calls `processAndOpenApp()` (`:64`).
-
-2. **`processAndOpenApp()`** — `ShareViewController.swift:70`
- Reads `extensionContext.inputItems`, then calls `processSharedImage(...)` (`:97`).
-
-3. **`processSharedImage(...)`** — `ShareViewController.swift:148`
- Loads the first image attachment, then `storeImageData(...)` (`:245`).
-
-4. **`storeImageData(...)`** — `ShareViewController.swift:292`
- Writes the image file into the App Group container and writes metadata keys
- (`sharedPhotoFilePath`, `sharedPhotoFileName`, `sharedPhotoShareId`) to
- `UserDefaults(suiteName: "group.app.trentlarson.timesafari.share")` (`:340–347`).
-
-5. **`setSharedPhotoReadyFlag()`** — `ShareViewController.swift:129` (called at `:108`)
- Sets `sharedPhotoReady = true` in the App Group UserDefaults (`:140`).
-
-6. **`openMainApp()`** — `ShareViewController.swift:356` (called at `:110`)
- Builds `URL(string: "timesafari://")` (`:362`), walks the responder chain to
- find a `UIApplication`, and calls `application.open(url, ...)` (`:373`).
- Fallback: `extensionContext?.open(url, ...)` (`:383`).
-
-7. **`context.completeRequest(...)`** — `ShareViewController.swift:119`
- Extension finishes; iOS hands the URL to the main app, launching the process.
-
-8. **Main app process launches → `AppDelegate.application(_:didFinishLaunchingWithOptions:)`**
- `AppDelegate.swift:11`.
- - Sets `UNUserNotificationCenter.delegate` (`:13`).
- - Schedules `SharedImagePlugin` registration on the main queue starting at
- **T+0.5s**, with up to 5 retries at increasing delays (`:21–40`,
- `registerSharedImagePlugin()` at `:46`). Registration requires the
- Capacitor bridge to exist (`:48–51`).
- - **Note:** `launchOptions` is received but is **never inspected** for a launch
- URL. AppDelegate does not read `launchOptions[.url]`.
-
-9. **Capacitor native bridge boots** (`CAPBridgeViewController` as `window.rootViewController`).
- The bridge loads the WKWebView and the JS bundle.
-
-10. **`AppDelegate.application(_:open:options:)`** — `AppDelegate.swift:133`
- When iOS delivers `timesafari://`, this proxies straight to
- `ApplicationDelegateProxy.shared.application(app, open: url, options:)` (`:138`).
- Capacitor's proxy is responsible for emitting the `appUrlOpen` event to JS —
- **but only to listeners that are already registered** (see §5).
-
-11. **JS startup — `src/main.capacitor.ts` executes top-to-bottom on bundle load:**
- - Logging banner (`:47–48`).
- - `const app = initializeApp();` (`:50`) → `src/main.common.ts:33`
- builds the Vue app, Pinia, axios, and **`app.use(router)`** (`main.common.ts:39`).
- The router itself is created at module import time in
- `src/router/index.ts:320` (`createRouter` with `createWebHistory("/")`).
- - `new DeepLinkHandler(router)` (`main.capacitor.ts:59`).
- - `app.mount("#app")` (`main.capacitor.ts:462`).
- - **Startup shared-image timer scheduled:** iOS uses `[1000]`ms delay
- (`:474–483`) → `checkForSharedImageAndNavigate()` at **T+1000ms**.
- - **`appStateChange` listener registered** (`:491–496`).
- - **Deep-link listener registration scheduled** via `setTimeout(..., 2000)`
- (`:500–510`) → `registerDeepLinkListener()` at **T+2000ms**.
-
-12. **`registerDeepLinkListener()`** — `main.capacitor.ts:298`
- Awaits `router.isReady()` (`:322`), then
- `CapacitorApp.addListener("appUrlOpen", handleDeepLink)` (`:329`).
- This is the **only** `appUrlOpen` registration in the codebase, and it occurs
- ~2 seconds after mount.
-
-### Cold-start order summary
-
-```
-ShareViewController.viewDidLoad
- → processAndOpenApp → processSharedImage → storeImageData (App Group file + metadata)
- → setSharedPhotoReadyFlag (sharedPhotoReady = true)
- → openMainApp → application.open("timesafari://")
- → completeRequest
-AppDelegate.didFinishLaunchingWithOptions (launchOptions URL ignored)
- → schedules SharedImagePlugin registration (T+0.5s, ≤5 retries)
-Capacitor bridge + WKWebView boot → JS bundle loads
-main.capacitor.ts (top-level):
- initializeApp() → router created/used → DeepLinkHandler → app.mount("#app")
- → schedule startup check (T+1000ms)
- → register appStateChange listener
- → schedule appUrlOpen registration (T+2000ms)
-AppDelegate.application(_:open:) → ApplicationDelegateProxy (appUrlOpen emitted)
-applicationDidBecomeActive → checkForSharedImageOnActivation (native flag path)
-```
-
-**Key cold-start fact:** because `application.open("timesafari://")` is delivered
-during/just after process launch, the Capacitor `appUrlOpen` event fires **before**
-the JS `appUrlOpen` listener is registered (registration is at T+2000ms). On cold
-start, the successful navigation is therefore driven by the **T+1000ms startup
-timer** (and/or `appStateChange`), not by `appUrlOpen`. See §6 and §7.
-
----
-
-## 2. Warm-start launch path
-
-"Warm start" = the main app process is already running (foreground or background)
-when the user taps Share.
-
-1. **Share Extension** runs the identical sequence as §1 steps 1–7
- (`storeImageData` → `setSharedPhotoReadyFlag` → `openMainApp` →
- `application.open("timesafari://")` → `completeRequest`).
-
-2. **iOS resumes the existing app process** (no new launch, no
- `didFinishLaunchingWithOptions`).
-
-3. **`AppDelegate.application(_:open:options:)`** — `AppDelegate.swift:133`
- Proxies to `ApplicationDelegateProxy.shared.application(...)` (`:138`).
- Because the JS `appUrlOpen` listener was registered during the earlier launch
- (T+2000ms after the first mount), Capacitor delivers the event to JS.
-
-4. **`AppDelegate.applicationDidBecomeActive`** — `AppDelegate.swift:77`
- Also fires on resume:
- - re-sets the notification delegate (`:81`),
- - calls `checkForSharedImageOnActivation()` (`:84`).
- `checkForSharedImageOnActivation()` (`:117`) reads `isSharedPhotoReady()`
- (`SharedImageUtility.swift:183`), clears the flag (`:121`), and posts the
- `SharedPhotoReady` NSNotification (`:125`). **No JS code listens for that
- NSNotification** (see §4 / §7) — it is a dead signal.
-
-5. **JS `appUrlOpen` → `handleDeepLink`** — `main.capacitor.ts:194`
- - `url === "timesafari://"` matches the empty-path branch (`:201`).
- - On iOS native (`:203–207`), calls `checkAndStoreNativeSharedImage()` (`:216`).
-
-6. **`checkAndStoreNativeSharedImage()`** — `main.capacitor.ts:131`
- - Guards against re-entrancy via `isProcessingSharedImage` (`:136`, `:143`).
- - Calls `SharedImage.getSharedImage()` (`:159`) → native
- `SharedImagePlugin.getSharedImage` (`SharedImagePlugin.swift:43`) →
- `SharedImageUtility.getSharedImageData()` (`SharedImageUtility.swift:51`)
- which reads the App Group file and returns `{ base64, fileName }`.
- - On success, `storeSharedImageInTempDB(...)` (`main.capacitor.ts:72`) writes
- the data URL into the SQLite `temp` table under `SHARED_PHOTO_BASE64_KEY`.
-
-7. **Navigation to `/shared-photo`** — `main.capacitor.ts:218–248`
- - `await router.isReady()` (`:224`).
- - If already on `/shared-photo`, `router.replace(...)` with a `_refresh`
- timestamp (`:234`); otherwise `router.push({ path: "/shared-photo", query: { fileName } })`
- (`:239`).
-
-### Warm-start callback chain
-
-```
-ShareViewController.openMainApp → application.open("timesafari://") → completeRequest
-iOS resumes running process
-AppDelegate.application(_:open:) → ApplicationDelegateProxy ── emits appUrlOpen ──┐
-AppDelegate.applicationDidBecomeActive → checkForSharedImageOnActivation │
- (reads + clears sharedPhotoReady, posts SharedPhotoReady NSNotification = no JS listener)
- │
-JS appUrlOpen listener (registered earlier) ←───────────────────────────────────────┘
- → handleDeepLink (empty-path branch)
- → checkAndStoreNativeSharedImage
- → SharedImage.getSharedImage → SharedImageUtility.getSharedImageData (App Group file)
- → storeSharedImageInTempDB (SQLite temp table)
- → router.isReady → router.push/replace("/shared-photo")
-```
-
-In parallel, the `appStateChange` listener (`main.capacitor.ts:491`) also fires
-on `isActive` and independently calls `checkForSharedImageAndNavigate()`. Both
-paths converge on the same `isProcessingSharedImage` lock and the same
-`/shared-photo` navigation.
-
----
-
-## 3. appUrlOpen audit
-
-### Registrations
-
-There is exactly **one** runtime registration of `appUrlOpen` in the codebase:
-
-- **`main.capacitor.ts:329`** inside `registerDeepLinkListener()`:
- ```ts
- const listenerHandle = await CapacitorApp.addListener("appUrlOpen", handleDeepLink);
- ```
- (`CapacitorApp` = `@capacitor/app`, imported at `main.capacitor.ts:32`.)
-
-Supporting / non-runtime references:
-- `capacitor.config.ts:11–19` declares the `App.appUrlOpen` handler for
- `timesafari://*` with `autoVerify: true` (config, not a JS listener).
-- `src/libs/capacitor/app.ts:32–35, 42–59` is a typed wrapper exposing
- `addListener("appUrlOpen", ...)`, but `main.capacitor.ts` calls
- `@capacitor/app` directly, **not** this wrapper.
-
-### When it is registered relative to startup
-
-- Scheduled by `setTimeout(..., 2000)` at `main.capacitor.ts:500`, i.e.
- **~2000 ms after `app.mount("#app")`** (`:462`).
-- Inside `registerDeepLinkListener()`, registration additionally waits for
- `await router.isReady()` (`:322`) before calling `addListener` (`:329`).
-
-### Handlers that execute because of it
-
-- **`handleDeepLink(data)`** — `main.capacitor.ts:194` is the only handler.
-
-### Calls made by the handler
-
-- **`handleDeepLink`** (`:194`)
- - **`checkAndStoreNativeSharedImage()`** (`:216`) — for empty-path
- `timesafari://` / `timesafari:///` on iOS native (`:201–207`).
- - **`router.isReady()`** (`:224`), then **`router.replace`** (`:234`) or
- **`router.push`** (`:239`) → navigation to `/shared-photo`.
- - For non-empty deep links: `router.isReady()` (`:264`) then
- `deepLinkHandler.handleDeepLink(url)` (`:269`) — the `DeepLinkHandler` class
- instance (`:59`). (Empty-path share URLs never reach this branch.)
-
-`handleDeepLink` is **not** the same function as `deepLinkHandler.handleDeepLink`
-(`src/services/deepLinks.ts`); the module-level function wraps the class method.
-
-### Call graph (appUrlOpen)
-
-```
-CapacitorApp.addListener("appUrlOpen", handleDeepLink) [main.capacitor.ts:329]
- │ (fires on timesafari:// open, warm start only in practice — see §6)
- ▼
-handleDeepLink(data) [:194]
- ├─ if url == "timesafari://" / "timesafari:///" and iOS native [:201–207]
- │ └─ checkAndStoreNativeSharedImage() [:216 → :131]
- │ └─ SharedImage.getSharedImage() [:159]
- │ └─ (native) getSharedImageData() [SharedImageUtility.swift:51]
- │ └─ storeSharedImageInTempDB() [:177 → :72]
- │ └─ router.isReady() [:224]
- │ └─ router.replace / router.push → /shared-photo [:234 / :239]
- │
- └─ else (non-empty deep link)
- └─ router.isReady() [:264]
- └─ deepLinkHandler.handleDeepLink(url) [:269]
-```
-
----
-
-## 4. Startup / shared-image detection audit
-
-Every place the app checks for a shared image, and when each runs:
-
-| # | Mechanism | Location | When it executes | Cold start? | Warm start? |
-| --- | --- | --- | --- | --- | --- |
-| A | **Startup timer** `setTimeout(..., 1000)` → `checkForSharedImageAndNavigate()` | `main.capacitor.ts:474–483` (iOS delays `[1000]`) | ~1000 ms after JS bundle loads / mount | **Yes** (primary cold-start path) | Only if bundle reloaded (normally no) |
-| B | **`appStateChange` listener** → `checkForSharedImageAndNavigate()` | `main.capacitor.ts:491–496` | Every time app becomes active (`isActive === true`) | Yes (initial activation can fire) | **Yes** (primary on resume) |
-| C | **`appUrlOpen` listener** → `handleDeepLink` → `checkAndStoreNativeSharedImage()` | `main.capacitor.ts:329`, handler `:194/:216` | On `timesafari://` open, **only if listener already registered** (T+2000ms) | Usually **No** (URL arrives before listener) | **Yes** |
-| D | **Native `applicationDidBecomeActive`** → `checkForSharedImageOnActivation()` | `AppDelegate.swift:77, 117` | Every activation (launch + resume) | Yes | Yes |
-| E | **Native ready-flag read** `isSharedPhotoReady()` / `clearSharedPhotoReadyFlag()` | `SharedImageUtility.swift:183, 195` (called from D) | Inside D | Yes | Yes |
-| F | **`SharedPhotoReady` NSNotification** posted | `AppDelegate.swift:125` | Inside D, when flag was set | Yes | Yes |
-
-### Detail per mechanism
-
-- **A — JS startup timer.** `main.capacitor.ts:474–483`. iOS uses a single
- `[1000]` ms delay (Android uses `[500, 1500, 3000]`). Calls
- `checkForSharedImageAndNavigate()` (`:353`), which calls
- `checkAndStoreNativeSharedImage()` (`:423`) and then pushes/replaces
- `/shared-photo` (`:439–449`). This is the mechanism that actually carries
- cold-start shares to `/shared-photo`.
-
-- **B — `appStateChange`.** `main.capacitor.ts:491`. Fires on every transition to
- active. Calls the same `checkForSharedImageAndNavigate()`. Primary warm-start /
- resume detector and a backstop for cold start.
-
-- **C — `appUrlOpen`.** Registered at `main.capacitor.ts:329` (T+2000ms after
- mount). Handler `handleDeepLink` (`:194`). Effective only when the listener is
- already registered when the URL arrives — i.e. warm starts.
-
-- **D — Native `applicationDidBecomeActive`.** `AppDelegate.swift:77`. Calls
- `checkForSharedImageOnActivation()` (`:117`).
-
-- **E — ready flag.** `checkForSharedImageOnActivation()` reads
- `SharedImageUtility.isSharedPhotoReady()` (`:119` → `SharedImageUtility.swift:183`)
- and clears it via `clearSharedPhotoReadyFlag()` (`:121` →
- `SharedImageUtility.swift:195`). The flag is **set** by the extension at
- `ShareViewController.swift:140`.
-
-- **F — `SharedPhotoReady` NSNotification.** Posted at `AppDelegate.swift:125`.
- **No JavaScript or Capacitor bridge code observes this notification anywhere in
- the repo** (only doc references and the post site exist). It is therefore a
- dead/no-op signal as far as JS navigation is concerned.
-
-### Polling / retry logic
-
-- **JS retry:** No active polling loop inside `checkAndStoreNativeSharedImage()`.
- The code comments at `main.capacitor.ts:213–214` mention "polling internally,"
- but the implementation (`:131–192`) makes a **single** `getSharedImage()` call.
- The only "retry-like" behavior on the JS side is the **multiple invocation
- surfaces** (A startup timer, B appStateChange, C appUrlOpen), all gated by the
- `isProcessingSharedImage` lock (`:62, :136, :143`).
-- **Native retry:** `AppDelegate.swift:21–40` retries **plugin registration**
- (not image detection) up to 5 times starting at T+0.5s.
-
----
-
-## 5. Native launch information
-
-### Does AppDelegate receive the launch URL before JavaScript is initialized?
-
-**No — the AppDelegate does not capture the launch URL at all.**
-
-- `AppDelegate.application(_:didFinishLaunchingWithOptions:)`
- (`AppDelegate.swift:11`) receives `launchOptions`, but the body
- (`:13–43`) **never reads `launchOptions[.url]`** or otherwise extracts a launch
- URL. It only configures notifications and schedules plugin registration.
-
-- The only URL entry point is `AppDelegate.application(_:open:options:)`
- (`AppDelegate.swift:133`), which immediately forwards to
- `ApplicationDelegateProxy.shared.application(app, open: url, options:)` (`:138`)
- with **no local storage** of the URL. Capacitor's proxy owns the URL from here.
-
-### Where is it stored?
-
-- It is **not** stored in the app's own native code. Whatever buffering exists is
- internal to Capacitor's `ApplicationDelegateProxy` / `@capacitor/app` plugin.
- The app code does not call `App.getLaunchUrl()` anywhere (only referenced in
- docs at `doc/native-share-target-implementation.md:436`).
-
-- The **share payload** (not the URL) is durably stored by the extension in the
- App Group container: the image file plus metadata keys at
- `ShareViewController.swift:340–347`, and the `sharedPhotoReady` boolean at
- `:140`. This payload is what the JS side later reads via
- `SharedImage.getSharedImage()`.
-
-### Is the URL forwarded to JS?
-
-- Only through Capacitor's `appUrlOpen` event, and only to listeners present at
- emit time. The single JS listener is registered at
- `main.capacitor.ts:329`, **~2000 ms after mount**.
-
-### Can the launch URL be lost before listeners are registered?
-
-- **Yes, the `appUrlOpen` event can be lost on cold start.** Because the URL is
- delivered through `application(_:open:)` during/just after launch, and the JS
- listener is registered at T+2000ms (`:500`), an event emitted before that point
- will have no JS listener — unless Capacitor buffers the launch URL until a
- listener attaches. The app code does not rely on (or verify) such buffering;
- there is no `getLaunchUrl()` call to recover a missed event.
-
-- **The share payload itself is NOT lost.** Because the image and metadata persist
- in the App Group container, the cold-start startup timer (mechanism A,
- `main.capacitor.ts:474`) and `appStateChange` (mechanism B, `:491`) can still
- retrieve it via `getSharedImage()` independent of whether the `appUrlOpen`
- event was delivered. The only thing at risk is the **URL event/signal**, not the
- data.
-
-### Complete native-launch flow
-
-```
-Extension writes App Group file + metadata + sharedPhotoReady=true [ShareViewController.swift:340,140]
-Extension: application.open("timesafari://") [ShareViewController.swift:373]
-iOS launches/resumes app
- ├─ didFinishLaunchingWithOptions(launchOptions) [AppDelegate.swift:11] ← launchOptions URL NOT read
- ├─ application(_:open:options:) [AppDelegate.swift:133] → ApplicationDelegateProxy (Capacitor buffers/emits appUrlOpen)
- └─ applicationDidBecomeActive [AppDelegate.swift:77] → checkForSharedImageOnActivation [:117]
- → isSharedPhotoReady [:119] → clear flag [:121] → post SharedPhotoReady [:125] (no JS listener)
-JS bundle loads later → appUrlOpen listener attaches at T+2000ms [main.capacitor.ts:329]
-```
-
----
-
-## 6. Timing analysis
-
-`T0` = moment the JS bundle begins executing / `app.mount("#app")`
-(`main.capacitor.ts:462`). Native launch precedes T0.
-
-### Cold start timeline
-
-| Time | Actor | Event | Reference |
-| --- | --- | --- | --- |
-| pre-launch | Extension | write file + metadata + `sharedPhotoReady=true`; `open("timesafari://")`; `completeRequest` | `ShareViewController.swift:340,140,373,119` |
-| launch | Native | `didFinishLaunchingWithOptions` (launch URL ignored) | `AppDelegate.swift:11` |
-| ~launch | Native | `application(_:open:)` → ApplicationDelegateProxy → (Capacitor) `appUrlOpen` emitted | `AppDelegate.swift:133` |
-| launch +0.5s..2.5s | Native | `SharedImagePlugin` registration (≤5 retries) | `AppDelegate.swift:21–40` |
-| ~launch | Native | `applicationDidBecomeActive` → `checkForSharedImageOnActivation` → clear flag + post `SharedPhotoReady` (dead) | `AppDelegate.swift:77,117,125` |
-| T0 | JS | bundle executes: `initializeApp`, router created/used, `DeepLinkHandler`, `app.mount` | `main.capacitor.ts:50,59,462`; `main.common.ts:39`; `router/index.ts:320` |
-| T0 | JS | register `appStateChange` listener | `main.capacitor.ts:491` |
-| T0 + ~1000ms | JS | **startup timer** → `checkForSharedImageAndNavigate` → `getSharedImage` → push `/shared-photo` | `main.capacitor.ts:474–483,353,423,449` |
-| T0 + ~2000ms | JS | `registerDeepLinkListener` → `await router.isReady` → `addListener("appUrlOpen")` | `main.capacitor.ts:500,322,329` |
-
-```
-Extension → openMainApp → AppDelegate(launch) → Capacitor bridge → JS bootstrap (T0)
- → router ready → [appUrlOpen registered @ T0+2000ms]
- → startup shared-image check @ T0+1000ms ──► getSharedImage ──► /shared-photo
- → applicationDidBecomeActive (native flag path, no JS effect)
-```
-
-**Cold-start race conditions / ordering dependencies:**
-
-1. **`appUrlOpen` arrives before its listener exists.** The URL is delivered at
- launch, but the listener attaches at T0+2000ms (`:500`/`:329`). Unless
- Capacitor buffers the launch URL, the `appUrlOpen` path does not fire on cold
- start. Cold-start success relies on the **T0+1000ms startup timer** instead.
-
-2. **Plugin registration vs. first `getSharedImage()`.** Plugin registration runs
- T+0.5s..~2.5s (`AppDelegate.swift:21–40`); the startup `getSharedImage()` call
- is at ~T0+1000ms. If the bridge/plugin is not yet registered when JS calls
- `getSharedImage()`, the call throws and is caught
- (`main.capacitor.ts:160–167`), returning `{ success: false }`. Recovery then
- depends on a later `appStateChange` firing.
-
-3. **Native flag cleared with no JS consumer.** `applicationDidBecomeActive`
- clears `sharedPhotoReady` and posts `SharedPhotoReady` (`AppDelegate.swift:121,125`),
- but no JS listens. Clearing the flag has no effect on JS navigation because JS
- reads the **file/metadata**, not the flag — so this does not cause loss, but
- the posted notification is inert.
-
-### Warm start timeline
-
-| Time | Actor | Event | Reference |
-| --- | --- | --- | --- |
-| t | Extension | write file + metadata + flag; `open("timesafari://")`; `completeRequest` | `ShareViewController.swift:340,140,373,119` |
-| t | Native | `application(_:open:)` → ApplicationDelegateProxy → `appUrlOpen` (listener already attached) | `AppDelegate.swift:133`; `main.capacitor.ts:329` |
-| t | Native | `applicationDidBecomeActive` → clear flag + post `SharedPhotoReady` (dead) | `AppDelegate.swift:77,121,125` |
-| t (≈same) | JS | `appStateChange(isActive)` → `checkForSharedImageAndNavigate` | `main.capacitor.ts:491` |
-| t (≈same) | JS | `appUrlOpen` → `handleDeepLink` → `checkAndStoreNativeSharedImage` | `main.capacitor.ts:194,216` |
-| t+ε | JS | `getSharedImage` → store temp DB → `router.push/replace("/shared-photo")` | `main.capacitor.ts:159,177,234/239` |
-
-```
-Extension → appUrlOpen (listener present) ─┐
- ├─► handleDeepLink / checkForSharedImageAndNavigate
-appStateChange(isActive) ──────────────────┘ → getSharedImage → /shared-photo
-```
-
-**Warm-start race conditions / ordering dependencies:**
-
-1. **Duplicate triggers.** `appUrlOpen` (C) and `appStateChange` (B) fire at
- nearly the same time, both calling `checkAndStoreNativeSharedImage()`. The
- `isProcessingSharedImage` boolean lock (`main.capacitor.ts:62,136,143`)
- prevents concurrent processing, but it is a simple non-reentrant flag: the
- second caller returns `{ success: false }` immediately and does not navigate,
- so navigation is driven by whichever caller wins. Because the lock is released
- synchronously at the end of each path, ordering determines which trigger
- performs the navigation.
-
-2. **Read-only native retrieval.** `getSharedImageData()`
- (`SharedImageUtility.swift:51`) leaves the file/metadata intact after reading
- (`:75–76`), so repeated reads from B and C return the same data rather than one
- "consuming" the other.
-
----
-
-## 7. Final summary
-
-(No code changes recommended — answers only.)
-
-1. **What mechanism actually causes successful warm-start shares to navigate to
- `/shared-photo`?**
- The **JS `appUrlOpen` listener** (`main.capacitor.ts:329`) firing
- `handleDeepLink` (`:194`), and/or the **`appStateChange` listener** (`:491`),
- each calling `checkAndStoreNativeSharedImage()` → `SharedImage.getSharedImage()`
- → `router.push/replace("/shared-photo")`. On a warm start the `appUrlOpen`
- listener already exists, so the deep-link path is available; `appStateChange`
- is a redundant parallel trigger. Both converge through the
- `isProcessingSharedImage` lock onto the same navigation.
-
-2. **What mechanism is supposed to cause successful cold-start shares to navigate
- to `/shared-photo`?**
- The **JS startup timer** at `main.capacitor.ts:474–483` (iOS `[1000]` ms) →
- `checkForSharedImageAndNavigate()` (`:353`) →
- `checkAndStoreNativeSharedImage()` (`:423`) → `getSharedImage()` (reads the App
- Group file/metadata persisted by the extension) → `router.push("/shared-photo")`
- (`:449`). `appStateChange` (`:491`) acts as a backstop. This path does **not**
- depend on the `appUrlOpen` event, because the `appUrlOpen` listener is not
- registered until ~T0+2000ms (`:500`), after the launch URL has already been
- delivered.
-
-3. **Are those mechanisms the same or different?**
- **Different.**
- - Warm start: driven primarily by the **`appUrlOpen` deep-link event**
- (`handleDeepLink`, `:194`/`:216`), with `appStateChange` as parallel backup.
- - Cold start: driven by the **startup `setTimeout` poll** (`:474`) /
- `appStateChange` (`:491`) calling `checkForSharedImageAndNavigate()`.
- They share the same downstream code (`checkAndStoreNativeSharedImage` →
- `getSharedImage` → router navigation) but are entered through **different
- triggers**, because the `appUrlOpen` event is unavailable during cold start.
-
-4. **Is there any point where a launch URL or launch signal could be lost before
- JavaScript is ready?**
- **Yes — the `appUrlOpen` URL event can be lost on cold start.** The launch URL
- is delivered to `AppDelegate.application(_:open:)` (`AppDelegate.swift:133`)
- and proxied into Capacitor at process launch, but the JS `appUrlOpen` listener
- is not registered until ~2000 ms after mount (`main.capacitor.ts:500,329`).
- If Capacitor does not buffer the launch URL until that listener attaches, the
- `appUrlOpen` event is dropped. Additionally, the native
- `SharedPhotoReady` NSNotification (`AppDelegate.swift:125`) is posted with **no
- JS/bridge listener**, so that signal is always lost.
- **However, the share payload (image + metadata in the App Group container,
- `ShareViewController.swift:340–347`) is durable and is not lost**; it is
- recovered by the startup timer / `appStateChange` calling
- `getSharedImage()`, which is why cold-start shares can still reach
- `/shared-photo` despite the `appUrlOpen` event being unavailable.