diff --git a/packages/firebase_analytics/firebase_analytics/ios/firebase_analytics/Sources/firebase_analytics/FirebaseAnalyticsPlugin.swift b/packages/firebase_analytics/firebase_analytics/ios/firebase_analytics/Sources/firebase_analytics/FirebaseAnalyticsPlugin.swift index 96b5921f88b0..287bb783a350 100644 --- a/packages/firebase_analytics/firebase_analytics/ios/firebase_analytics/Sources/firebase_analytics/FirebaseAnalyticsPlugin.swift +++ b/packages/firebase_analytics/firebase_analytics/ios/firebase_analytics/Sources/firebase_analytics/FirebaseAnalyticsPlugin.swift @@ -9,6 +9,7 @@ import StoreKit import FlutterMacOS #else import Flutter + import UIKit #endif #if canImport(firebase_core) @@ -47,6 +48,9 @@ public class FirebaseAnalyticsPlugin: NSObject, FLTFirebasePluginProtocol, Flutt let instance = FirebaseAnalyticsPlugin() FirebaseAnalyticsHostApiSetup.setUp(binaryMessenger: binaryMessenger, api: instance) + #if os(iOS) + registerSceneDelegateIfAvailable(registrar, instance: instance) + #endif } func logEvent(event: [String: Any?], completion: @escaping (Result) -> Void) { @@ -296,3 +300,73 @@ public class FirebaseAnalyticsPlugin: NSObject, FLTFirebasePluginProtocol, Flutt FLTFirebaseAnalyticsChannelName } } + +#if os(iOS) + /// Registers for UIScene callbacks when the host Flutter SDK supports it. + /// + /// `addSceneDelegate(_:)` and `FlutterSceneLifeCycleDelegate` were added in + /// Flutter 3.38. Calling via `responds(to:)` keeps this plugin compiling on + /// older Flutter versions; the methods below are unused until registration + /// succeeds. + private func registerSceneDelegateIfAvailable( + _ registrar: FlutterPluginRegistrar, + instance: FirebaseAnalyticsPlugin + ) { + let selector = NSSelectorFromString("addSceneDelegate:") + guard (registrar as AnyObject).responds(to: selector) else { + return + } + _ = (registrar as AnyObject).perform(selector, with: instance) + } + + /// Forwards UIScene URL and universal-link opens to Analytics. + /// + /// The native SDK only swizzles `UIApplicationDelegate`. Under UIScene, UIKit + /// delivers those opens here instead, so campaign attribution (`firebase_campaign`) + /// is lost unless we call `handleOpen` / `handleUserActivity` ourselves. + /// + /// Always returns `false` so later plugins (Auth, Messaging, etc.) still receive + /// the same scene events. Selectors match `FlutterSceneLifeCycleDelegate` + /// without conforming to that protocol, which is absent from older Flutter SDKs. + extension FirebaseAnalyticsPlugin { + @objc(scene:willConnectToSession:options:) + public func scene( + _: UIScene, + willConnectTo _: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions? + ) -> Bool { + handleCampaign(urlContexts: connectionOptions?.urlContexts) + handleCampaign(userActivities: connectionOptions?.userActivities) + return false + } + + @objc(scene:openURLContexts:) + public func scene( + _: UIScene, + openURLContexts urlContexts: Set + ) -> Bool { + handleCampaign(urlContexts: urlContexts) + return false + } + + @objc(scene:continueUserActivity:) + public func scene(_: UIScene, continue userActivity: NSUserActivity) -> Bool { + Analytics.handleUserActivity(userActivity) + return false + } + + private func handleCampaign(urlContexts: Set?) { + guard let urlContexts else { return } + for context in urlContexts { + Analytics.handleOpen(context.url) + } + } + + private func handleCampaign(userActivities: Set?) { + guard let userActivities else { return } + for activity in userActivities { + Analytics.handleUserActivity(activity) + } + } + } +#endif