Compare commits

...

3 Commits

Author SHA1 Message Date
Jose Olarte III
02e6e3427d refactor(ios): remove SharedImage plugin-readiness polling (Phase 2B-3)
The SharedImage plugin is now registered deterministically from
AppBridgeViewController.capacitorDidLoad() before the web layer loads, so
JS no longer needs to wait for it. Remove the readiness machinery that
existed solely to tolerate the old async AppDelegate registration:
waitForSharedImagePluginReady(), the sleep() helper, the
STARTUP_PLUGIN_MAX_ATTEMPTS/RETRY_DELAY_MS constants and retry loop, and
the three readiness-only console diagnostics (waiting / ready after N /
giving up).

The iOS startup branch now calls checkForSharedImageAndNavigate()
immediately. All other share-target behavior is unchanged: cold-start,
extension, and launch diagnostics; native trace APIs; the Share Target
Debug Panel; appStateChange/appUrlOpen handling; actual shared-image
handling; and the Android startup path. No JS readiness diagnostics
remain; no Android changes.
2026-06-26 21:14:59 +08:00
Jose Olarte III
337a8f7536 refactor(ios): remove AppDelegate SharedImage registration (Phase 2B-2)
Now that AppBridgeViewController registers SharedImagePlugin from
capacitorDidLoad(), remove the obsolete plugin-registration logic from
AppDelegate: the 5-attempt retry loop in didFinishLaunchingWithOptions,
the registerSharedImagePlugin() method, and the registration-specific
logging. SharedImagePlugin is now registered from a single, deterministic
site.

All non-registration responsibilities are unchanged: notification
handling, URL and universal-link proxying, SharedPhotoReady activation
logic, every lifecycle callback, the temporary app launch tracing, and
all temporary share-target diagnostics remain intact. No JS or Android
changes.
2026-06-26 19:35:43 +08:00
Jose Olarte III
4978e93711 feat(ios): register SharedImage via CAPBridgeViewController subclass (Phase 2B-1)
Introduce AppBridgeViewController, a CAPBridgeViewController subclass that
registers the app-local SharedImagePlugin from capacitorDidLoad(), where
the Capacitor bridge is guaranteed to exist. This is the first step toward
replacing AppDelegate's timed retry registration with a deterministic,
bridge-ready registration point.

Point the existing root bridge controller in Main.storyboard at
AppBridgeViewController (same VC id/scene; only the custom class changes)
and add the new file to the App target in project.pbxproj.

The existing AppDelegate registration and its retry loop are intentionally
left in place as a temporary safety net, so the plugin is now registered
from both sites during this phase. No diagnostics, retry logic, JS, or
Android code changed; plugin name ("SharedImage") and implementation are
unchanged.
2026-06-26 19:26:00 +08:00
5 changed files with 44 additions and 116 deletions

View File

@@ -18,6 +18,7 @@
C86585DF2ED456DE00824752 /* TimeSafariShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = C86585D52ED456DE00824752 /* TimeSafariShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
C8C56E142EE0474B00737D0E /* SharedImageUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8C56E132EE0474B00737D0E /* SharedImageUtility.swift */; };
C8C56E162EE064CB00737D0E /* SharedImagePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8C56E152EE064CA00737D0E /* SharedImagePlugin.swift */; };
C8C56E182EE0700A00737D0E /* AppBridgeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8C56E172EE0700A00737D0E /* AppBridgeViewController.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -59,6 +60,7 @@
C86585E52ED4577F00824752 /* App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = App.entitlements; sourceTree = "<group>"; };
C8C56E132EE0474B00737D0E /* SharedImageUtility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedImageUtility.swift; sourceTree = "<group>"; };
C8C56E152EE064CA00737D0E /* SharedImagePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedImagePlugin.swift; sourceTree = "<group>"; };
C8C56E172EE0700A00737D0E /* AppBridgeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppBridgeViewController.swift; sourceTree = "<group>"; };
E2E9297D5D02C549106C77F9 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; };
EAEC6436E595F7CD3A1C9E96 /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -127,6 +129,7 @@
504EC3061FED79650016851F /* App */ = {
isa = PBXGroup;
children = (
C8C56E172EE0700A00737D0E /* AppBridgeViewController.swift */,
C8C56E152EE064CA00737D0E /* SharedImagePlugin.swift */,
C8C56E132EE0474B00737D0E /* SharedImageUtility.swift */,
C86585E52ED4577F00824752 /* App.entitlements */,
@@ -346,6 +349,7 @@
buildActionMask = 2147483647;
files = (
C8C56E162EE064CB00737D0E /* SharedImagePlugin.swift in Sources */,
C8C56E182EE0700A00737D0E /* AppBridgeViewController.swift in Sources */,
504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
C8C56E142EE0474B00737D0E /* SharedImageUtility.swift in Sources */,
);

View File

@@ -0,0 +1,30 @@
//
// AppBridgeViewController.swift
// App
//
// Capacitor bridge view controller subclass.
//
// Phase 2B-1: registers the app-local SharedImagePlugin from the deterministic
// capacitorDidLoad() lifecycle callback, where the Capacitor bridge is
// guaranteed to exist. The existing AppDelegate registration is intentionally
// left in place as a temporary safety net during this phase.
//
import UIKit
import Capacitor
class AppBridgeViewController: CAPBridgeViewController {
override func capacitorDidLoad() {
super.capacitorDidLoad()
// Register the app-local SharedImage plugin using the same approach as
// AppDelegate. The @objc(SharedImage) annotation exposes it as
// "SharedImage" to JavaScript. At this point the bridge is guaranteed
// to be available (capacitorDidLoad runs immediately after the bridge
// is created).
let pluginInstance = SharedImagePlugin()
bridge?.registerPluginInstance(pluginInstance)
print("[AppBridgeViewController] ✅ Registered SharedImagePlugin (exposed as 'SharedImage' via @objc annotation)")
}
}

View File

@@ -22,50 +22,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
// Initialize SQLite
//let sqlite = SQLite()
//sqlite.initialize()
// Register SharedImage plugin manually after bridge is ready
// Try multiple times with increasing delays to ensure bridge is initialized
var attempts = 0
let maxAttempts = 5
func tryRegister() {
attempts += 1
if registerSharedImagePlugin() {
print("[AppDelegate] ✅ Plugin registration successful on attempt \(attempts)")
} else if attempts < maxAttempts {
DispatchQueue.main.asyncAfter(deadline: .now() + Double(attempts) * 0.5) {
tryRegister()
}
} else {
print("[AppDelegate] ⚠️ Failed to register plugin after \(maxAttempts) attempts")
}
}
// Start registration attempts
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
tryRegister()
}
// SharedImagePlugin is registered from AppBridgeViewController.capacitorDidLoad()
// (Phase 2B-2). The previous AppDelegate retry-based registration was removed.
// Override point for customization after application launch.
return true
}
@discardableResult
private func registerSharedImagePlugin() -> Bool {
guard let window = self.window,
let bridgeVC = window.rootViewController as? CAPBridgeViewController,
let bridge = bridgeVC.bridge else {
return false
}
// Create plugin instance
// The @objc(SharedImage) annotation makes it available as "SharedImage" to Objective-C
// which matches the JavaScript registration name
let pluginInstance = SharedImagePlugin()
bridge.registerPluginInstance(pluginInstance)
print("[AppDelegate] ✅ Registered SharedImagePlugin (exposed as 'SharedImage' via @objc annotation)")
return true
}
func applicationWillResignActive(_ application: UIApplication) {
// TEMPORARY SHARE TARGET DIAGNOSTICS

View File

@@ -11,7 +11,7 @@
<!--Bridge View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="CAPBridgeViewController" customModule="Capacitor" sceneMemberID="viewController"/>
<viewController id="BYZ-38-t0r" customClass="AppBridgeViewController" customModule="App" customModuleProvider="target" sceneMemberID="viewController"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>

View File

@@ -471,71 +471,6 @@ logger.log("[Capacitor] 🚀 Mounting app");
app.mount("#app");
logger.info(`[Main] ✅ App mounted successfully`);
/**
* Phase 2A: deterministic startup readiness for the SharedImage plugin (iOS).
*
* The native SharedImage plugin is registered asynchronously by AppDelegate
* (with its own retry budget), so a fixed startup delay cannot guarantee the
* plugin is reachable from JS before the first shared-image check. Instead of
* assuming readiness after an arbitrary sleep, we probe the plugin with a
* lightweight, read-only hasSharedImage() call and retry for a short bounded
* period until it responds or the budget is exhausted.
*
* This applies ONLY to the very first startup shared-image check. The
* appStateChange and appUrlOpen paths are intentionally left unchanged.
*/
const STARTUP_PLUGIN_MAX_ATTEMPTS = 10;
const STARTUP_PLUGIN_RETRY_DELAY_MS = 300;
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Wait until the iOS SharedImage native plugin is available.
*
* Probes the plugin with a read-only hasSharedImage() call; a successful
* resolution means the native plugin instance is registered and reachable.
* Retries up to STARTUP_PLUGIN_MAX_ATTEMPTS with STARTUP_PLUGIN_RETRY_DELAY_MS
* between attempts.
*
* @returns true once the plugin responds, false if the retry budget is exhausted
*/
async function waitForSharedImagePluginReady(): Promise<boolean> {
// TEMPORARY SHARE TARGET DIAGNOSTICS
console.info(
"[ShareTarget] Startup shared-image check waiting for SharedImage plugin",
);
for (let attempt = 1; attempt <= STARTUP_PLUGIN_MAX_ATTEMPTS; attempt++) {
try {
// Lightweight, read-only probe. hasSharedImage() does not consume or
// mutate the pending share, so probing is side-effect free.
await SharedImage.hasSharedImage();
// TEMPORARY SHARE TARGET DIAGNOSTICS
console.info(
`[ShareTarget] SharedImage plugin ready after ${attempt} attempt(s)`,
);
return true;
} catch (error) {
// Plugin not registered yet; wait and retry within the bounded budget.
logger.debug(
`[Main] SharedImage plugin not ready (attempt ${attempt}/${STARTUP_PLUGIN_MAX_ATTEMPTS}):`,
error instanceof Error ? error.message : String(error),
);
if (attempt < STARTUP_PLUGIN_MAX_ATTEMPTS) {
await sleep(STARTUP_PLUGIN_RETRY_DELAY_MS);
}
}
}
// TEMPORARY SHARE TARGET DIAGNOSTICS
console.info(
`[ShareTarget] Startup shared-image check giving up after ${STARTUP_PLUGIN_MAX_ATTEMPTS} attempt(s)`,
);
return false;
}
// Check for shared image on initial load (in case app was launched from share sheet)
// On Android, share intents are processed in MainActivity.onCreate, so we need to check
// after a delay to ensure the native code has finished processing
@@ -550,15 +485,11 @@ if (Capacitor.isNativePlatform() && Capacitor.getPlatform() === "android") {
}, delay);
});
} else if (Capacitor.isNativePlatform() && Capacitor.getPlatform() === "ios") {
// Phase 2A: replace the fixed 1000ms delay with a deterministic wait for the
// SharedImage plugin. The very first startup check runs only after the plugin
// is confirmed available; if the bounded retry budget is exhausted we still
// attempt once as a best-effort fallback (appStateChange retries on the next
// activation), so startup is never worse than the previous fixed-delay path.
void (async () => {
await waitForSharedImagePluginReady();
await checkForSharedImageAndNavigate();
})();
// Phase 2B-3: the SharedImage plugin is now registered deterministically from
// AppBridgeViewController.capacitorDidLoad() before the web layer loads, so it
// is guaranteed to exist here. Perform the initial shared-image check
// immediately without waiting/polling for plugin readiness.
void checkForSharedImageAndNavigate();
}
// Listen for app state changes to detect when app becomes active