diff --git a/ios/App/App/AppDelegate.swift b/ios/App/App/AppDelegate.swift index bfc0d05e..b1becbab 100644 --- a/ios/App/App/AppDelegate.swift +++ b/ios/App/App/AppDelegate.swift @@ -9,6 +9,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // TEMPORARY SHARE TARGET DIAGNOSTICS + let launchURL = launchOptions?[.url] as? URL + // TEMPORARY SHARE TARGET DIAGNOSTICS + SharedImageUtility.appendAppLaunchTrace( + "didFinishLaunchingWithOptions urlSupplied=\(launchURL != nil) url=\(launchURL?.absoluteString ?? "nil") matchesTimesafari=\(AppDelegate.isTimesafariURL(launchURL)) launchOptionsHasURL=\(launchURL != nil) sharedImageActivationInvoked=false" + ) + // Set notification center delegate so notifications show in foreground and rollover is triggered UNUserNotificationCenter.current().delegate = self @@ -61,20 +68,36 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } func applicationWillResignActive(_ application: UIApplication) { + // TEMPORARY SHARE TARGET DIAGNOSTICS + SharedImageUtility.appendAppLaunchTrace( + "applicationWillResignActive urlSupplied=false url=nil matchesTimesafari=false launchOptionsHasURL=false sharedImageActivationInvoked=false" + ) // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. } func applicationDidEnterBackground(_ application: UIApplication) { + // TEMPORARY SHARE TARGET DIAGNOSTICS + SharedImageUtility.appendAppLaunchTrace( + "applicationDidEnterBackground urlSupplied=false url=nil matchesTimesafari=false launchOptionsHasURL=false sharedImageActivationInvoked=false" + ) // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. } func applicationWillEnterForeground(_ application: UIApplication) { + // TEMPORARY SHARE TARGET DIAGNOSTICS + SharedImageUtility.appendAppLaunchTrace( + "applicationWillEnterForeground urlSupplied=false url=nil matchesTimesafari=false launchOptionsHasURL=false sharedImageActivationInvoked=false" + ) // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. } func applicationDidBecomeActive(_ application: UIApplication) { + // TEMPORARY SHARE TARGET DIAGNOSTICS + SharedImageUtility.appendAppLaunchTrace( + "applicationDidBecomeActive urlSupplied=false url=nil matchesTimesafari=false launchOptionsHasURL=false sharedImageActivationInvoked=true" + ) // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. // Re-set notification delegate when app becomes active (in case Capacitor resets it) @@ -116,7 +139,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD */ private func checkForSharedImageOnActivation() { // Check if shared photo is ready - if SharedImageUtility.isSharedPhotoReady() { + // TEMPORARY SHARE TARGET DIAGNOSTICS + let isReady = SharedImageUtility.isSharedPhotoReady() + // TEMPORARY SHARE TARGET DIAGNOSTICS + SharedImageUtility.appendAppLaunchTrace( + "checkForSharedImageOnActivation urlSupplied=false url=nil matchesTimesafari=false launchOptionsHasURL=false sharedImageActivationInvoked=true sharedPhotoReady=\(isReady)" + ) + if isReady { // Clear the flag SharedImageUtility.clearSharedPhotoReadyFlag() @@ -127,10 +156,18 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } func applicationWillTerminate(_ application: UIApplication) { + // TEMPORARY SHARE TARGET DIAGNOSTICS + SharedImageUtility.appendAppLaunchTrace( + "applicationWillTerminate urlSupplied=false url=nil matchesTimesafari=false launchOptionsHasURL=false sharedImageActivationInvoked=false" + ) // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. } func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { + // TEMPORARY SHARE TARGET DIAGNOSTICS + SharedImageUtility.appendAppLaunchTrace( + "application(open:) urlSupplied=true url=\(url.absoluteString) matchesTimesafari=\(AppDelegate.isTimesafariURL(url)) launchOptionsHasURL=false sharedImageActivationInvoked=false" + ) // Called when the app was launched with a url. Feel free to add additional processing here, // but if you want the App API to support tracking app url opens, make sure to keep this call // Note: Share Extension opens app with timesafari:// (empty path), which is handled by JavaScript @@ -138,6 +175,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD return ApplicationDelegateProxy.shared.application(app, open: url, options: options) } + // TEMPORARY SHARE TARGET DIAGNOSTICS + /// Returns true when the supplied URL uses the timesafari:// scheme. + /// Diagnostics-only helper; does not affect URL handling. + private static func isTimesafariURL(_ url: URL?) -> Bool { + return url?.scheme?.lowercased() == "timesafari" + } + func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { // Called when the app was launched with an activity, including Universal Links. // Feel free to add additional processing here, but if you want the App API to support diff --git a/ios/App/App/SharedImagePlugin.swift b/ios/App/App/SharedImagePlugin.swift index 19444207..627ac830 100644 --- a/ios/App/App/SharedImagePlugin.swift +++ b/ios/App/App/SharedImagePlugin.swift @@ -29,7 +29,11 @@ public class SharedImagePlugin: CAPPlugin, CAPBridgedPlugin { // TEMPORARY SHARE TARGET DIAGNOSTICS CAPPluginMethod(#selector(getShareExtensionTrace(_:)), returnType: .promise), // TEMPORARY SHARE TARGET DIAGNOSTICS - CAPPluginMethod(#selector(clearShareExtensionTrace(_:)), returnType: .promise) + CAPPluginMethod(#selector(clearShareExtensionTrace(_:)), returnType: .promise), + // TEMPORARY SHARE TARGET DIAGNOSTICS + CAPPluginMethod(#selector(getAppLaunchTrace(_:)), returnType: .promise), + // TEMPORARY SHARE TARGET DIAGNOSTICS + CAPPluginMethod(#selector(clearAppLaunchTrace(_:)), returnType: .promise) ] } @@ -93,5 +97,24 @@ public class SharedImagePlugin: CAPPlugin, CAPBridgedPlugin { SharedImageUtility.clearShareExtensionTrace() call.resolve() } + + // TEMPORARY SHARE TARGET DIAGNOSTICS + /** + * Return the raw app launch lifecycle trace log from the App Group container + */ + @objc public func getAppLaunchTrace(_ call: CAPPluginCall) { + call.resolve([ + "trace": SharedImageUtility.getAppLaunchTrace() + ]) + } + + // TEMPORARY SHARE TARGET DIAGNOSTICS + /** + * Delete the app launch lifecycle trace log if present + */ + @objc public func clearAppLaunchTrace(_ call: CAPPluginCall) { + SharedImageUtility.clearAppLaunchTrace() + call.resolve() + } } diff --git a/ios/App/App/SharedImageUtility.swift b/ios/App/App/SharedImageUtility.swift index 72e2611b..fb8e022e 100644 --- a/ios/App/App/SharedImageUtility.swift +++ b/ios/App/App/SharedImageUtility.swift @@ -18,6 +18,8 @@ public class SharedImageUtility { private static let sharedPhotoReadyKey = "sharedPhotoReady" // TEMPORARY SHARE TARGET DIAGNOSTICS private static let shareExtensionTraceFileName = "share-extension-trace.log" + // TEMPORARY SHARE TARGET DIAGNOSTICS + private static let appLaunchTraceFileName = "app-launch-trace.log" /// Get the App Group container URL for accessing shared files private static var appGroupContainerURL: URL? { @@ -174,6 +176,54 @@ public class SharedImageUtility { try? FileManager.default.removeItem(at: fileURL) } + // TEMPORARY SHARE TARGET DIAGNOSTICS + /** + * Append a single timestamped line to the app launch trace file in the + * App Group container. Each line is prefixed with an ISO8601 timestamp. + * Append-only; logging failures are intentionally swallowed (diagnostics only). + */ + static func appendAppLaunchTrace(_ message: String) { + guard let containerURL = appGroupContainerURL else { return } + let fileURL = containerURL.appendingPathComponent(appLaunchTraceFileName) + let timestamp = ISO8601DateFormatter().string(from: Date()) + let line = "\(timestamp) \(message)\n" + guard let data = line.data(using: .utf8) else { return } + if let handle = try? FileHandle(forWritingTo: fileURL) { + defer { try? handle.close() } + _ = try? handle.seekToEnd() + try? handle.write(contentsOf: data) + } else { + try? data.write(to: fileURL, options: .atomic) + } + } + + // TEMPORARY SHARE TARGET DIAGNOSTICS + /** + * Read the app launch trace file from the App Group container. + * Read-only: does not modify or delete the trace file. + * + * @returns the full trace contents, or an empty string if no trace exists + */ + static func getAppLaunchTrace() -> String { + guard let containerURL = appGroupContainerURL else { + return "" + } + let fileURL = containerURL.appendingPathComponent(appLaunchTraceFileName) + return (try? String(contentsOf: fileURL, encoding: .utf8)) ?? "" + } + + // TEMPORARY SHARE TARGET DIAGNOSTICS + /** + * Delete the app launch trace file from the App Group container if present. + */ + static func clearAppLaunchTrace() { + guard let containerURL = appGroupContainerURL else { + return + } + let fileURL = containerURL.appendingPathComponent(appLaunchTraceFileName) + try? FileManager.default.removeItem(at: fileURL) + } + /** * Check if shared photo ready flag is set * This flag is set by the Share Extension when image is ready diff --git a/src/main.capacitor.ts b/src/main.capacitor.ts index e22faa3a..fa1c6b44 100644 --- a/src/main.capacitor.ts +++ b/src/main.capacitor.ts @@ -417,6 +417,15 @@ async function checkForSharedImageAndNavigate() { "[ShareTarget] TRACE FIRST 500\n" + traceResult.trace.slice(0, 500), ); } + + // TEMPORARY SHARE TARGET DIAGNOSTICS + const launchTraceResult = await SharedImage.getAppLaunchTrace(); + // TEMPORARY SHARE TARGET DIAGNOSTICS + console.info("[ShareTarget] APP LAUNCH TRACE START"); + // TEMPORARY SHARE TARGET DIAGNOSTICS + console.info(launchTraceResult.trace); + // TEMPORARY SHARE TARGET DIAGNOSTICS + console.info("[ShareTarget] APP LAUNCH TRACE END"); } logger.debug("[Main] 🔍 Checking for shared image on app activation"); diff --git a/src/plugins/SharedImagePlugin.web.ts b/src/plugins/SharedImagePlugin.web.ts index 7536f66a..6671503e 100644 --- a/src/plugins/SharedImagePlugin.web.ts +++ b/src/plugins/SharedImagePlugin.web.ts @@ -10,6 +10,8 @@ import type { ShareExtensionDiagnostics, // TEMPORARY SHARE TARGET DIAGNOSTICS ShareExtensionTrace, + // TEMPORARY SHARE TARGET DIAGNOSTICS + AppLaunchTrace, } from "./definitions"; export class SharedImagePluginWeb @@ -45,4 +47,14 @@ export class SharedImagePluginWeb async clearShareExtensionTrace(): Promise { // Web platform doesn't support native sharing - no-op } + + // TEMPORARY SHARE TARGET DIAGNOSTICS + async getAppLaunchTrace(): Promise { + return { trace: "" }; + } + + // TEMPORARY SHARE TARGET DIAGNOSTICS + async clearAppLaunchTrace(): Promise { + // Web platform doesn't support native launch tracing - no-op + } } diff --git a/src/plugins/definitions.ts b/src/plugins/definitions.ts index c240e834..c0b2c29b 100644 --- a/src/plugins/definitions.ts +++ b/src/plugins/definitions.ts @@ -21,6 +21,11 @@ export interface ShareExtensionTrace { trace: string; } +// TEMPORARY SHARE TARGET DIAGNOSTICS +export interface AppLaunchTrace { + trace: string; +} + export interface SharedImagePlugin { /** * Get shared image data from native layer @@ -51,4 +56,16 @@ export interface SharedImagePlugin { * Delete the Share Extension execution trace log if present (iOS) */ clearShareExtensionTrace(): Promise; + + // TEMPORARY SHARE TARGET DIAGNOSTICS + /** + * Read the raw app launch lifecycle trace log (iOS) + */ + getAppLaunchTrace(): Promise; + + // TEMPORARY SHARE TARGET DIAGNOSTICS + /** + * Delete the app launch lifecycle trace log if present (iOS) + */ + clearAppLaunchTrace(): Promise; }