From 6d61c6f94d0f5e0c173feb87859980acf8946720 Mon Sep 17 00:00:00 2001 From: Jude Kwashie Date: Tue, 18 Aug 2026 08:49:49 +0000 Subject: [PATCH 1/6] feat(analytics,ios): add UIScene support for campaign attribution Forward scene URL and user-activity callbacks to Analytics.handleOpen / handleUserActivity so firebase_campaign is recorded when the host app uses UIScene. --- .../FirebaseAnalyticsPlugin.swift | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) 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..432ca0907d36 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 From 64d7a048fd26679f612e888d3de5588ed33e2e10 Mon Sep 17 00:00:00 2001 From: Jude Kwashie Date: Tue, 18 Aug 2026 08:57:04 +0000 Subject: [PATCH 2/6] fix(analytics,ios): use lowerCamelCase for openURLContexts parameter --- .../Sources/firebase_analytics/FirebaseAnalyticsPlugin.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 432ca0907d36..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 @@ -343,9 +343,9 @@ public class FirebaseAnalyticsPlugin: NSObject, FLTFirebasePluginProtocol, Flutt @objc(scene:openURLContexts:) public func scene( _: UIScene, - openURLContexts URLContexts: Set + openURLContexts urlContexts: Set ) -> Bool { - handleCampaign(urlContexts: URLContexts) + handleCampaign(urlContexts: urlContexts) return false } From 8f405af9a671c67c61105d99b8de2296cd68726b Mon Sep 17 00:00:00 2001 From: Jude Kwashie Date: Wed, 26 Aug 2026 15:12:31 +0000 Subject: [PATCH 3/6] test(analytics,ios): add live e2e coverage for UIScene campaign attribution Drive the scene handlers against the live Analytics SDK so CI exercises handleOpen / handleUserActivity, which BigQuery cannot observe in time. --- .../example/integration_test/e2e_test.dart | 96 ++++++++++++++ .../example/ios/Runner/Info.plist | 13 ++ .../FirebaseAnalyticsPlugin.swift | 122 +++++++++++++++++- 3 files changed, 228 insertions(+), 3 deletions(-) diff --git a/packages/firebase_analytics/firebase_analytics/example/integration_test/e2e_test.dart b/packages/firebase_analytics/firebase_analytics/example/integration_test/e2e_test.dart index c8eb946c6e8c..e8dcbe5312eb 100644 --- a/packages/firebase_analytics/firebase_analytics/example/integration_test/e2e_test.dart +++ b/packages/firebase_analytics/firebase_analytics/example/integration_test/e2e_test.dart @@ -433,5 +433,101 @@ void main() { defaultTargetPlatform != TargetPlatform.macOS), ); }); + + // firebase_campaign is recorded by the native SDK and is only visible in + // BigQuery (not DebugView), so CI cannot assert the exported event. These + // tests drive the same UIScene handlers the plugin registers and confirm + // Analytics.handleOpen / handleUserActivity run against the live project. + group( + 'UIScene campaign attribution', + () { + const channel = MethodChannel( + 'plugins.flutter.io/firebase_analytics_test', + ); + + String campaignUrl() { + final campaign = 'uiscene_${DateTime.now().microsecondsSinceEpoch}'; + return 'https://flutterfire.dev/?utm_source=flutterfire_e2e' + '&utm_medium=integration_test&utm_campaign=$campaign'; + } + + setUp(() async { + await FirebaseAnalytics.instance.setAnalyticsCollectionEnabled(true); + await FirebaseAnalytics.instance.setConsent( + analyticsStorageConsentGranted: true, + adStorageConsentGranted: true, + ); + }); + + test('registers as a Flutter scene delegate', () async { + final registered = + await channel.invokeMethod('isSceneDelegateRegistered'); + expect(registered, isTrue); + }); + + test( + 'handleOpen forwards a campaign URL to the live Analytics SDK', + () async { + final url = campaignUrl(); + await channel.invokeMethod('handleCampaignUrl', url); + + final handled = await channel.invokeMethod( + 'getLastHandledCampaignUrl', + ); + expect(handled, url); + + await expectLater( + FirebaseAnalytics.instance.logEvent( + name: 'uiscene_campaign_followup', + parameters: {'source': 'e2e'}, + ), + completes, + ); + }, + ); + + test( + 'scene continueUserActivity forwards a universal link', + () async { + final url = campaignUrl(); + final handled = await channel.invokeMethod( + 'handleCampaignUserActivity', + url, + ); + expect(handled, url); + }, + ); + + test( + 'scene openURLContexts receives a campaign URL opened on the live app', + () async { + final campaign = 'uiscene_${DateTime.now().microsecondsSinceEpoch}'; + final url = + 'ffa-e2e://campaign?utm_source=flutterfire_e2e&utm_medium=integration_test&utm_campaign=$campaign'; + + final opened = await channel.invokeMethod('openUrl', url); + expect(opened, isTrue); + + String? handled; + for (var i = 0; i < 25; i++) { + handled = await channel.invokeMethod( + 'getLastHandledCampaignUrl', + ); + if (handled == url) { + break; + } + await Future.delayed(const Duration(milliseconds: 200)); + } + expect( + handled, + url, + reason: + 'UIScene should deliver the custom-scheme campaign URL to the plugin', + ); + }, + ); + }, + skip: kIsWeb || defaultTargetPlatform != TargetPlatform.iOS, + ); }); } diff --git a/packages/firebase_analytics/firebase_analytics/example/ios/Runner/Info.plist b/packages/firebase_analytics/firebase_analytics/example/ios/Runner/Info.plist index 248bf3883db1..afff5b105a16 100755 --- a/packages/firebase_analytics/firebase_analytics/example/ios/Runner/Info.plist +++ b/packages/firebase_analytics/firebase_analytics/example/ios/Runner/Info.plist @@ -49,6 +49,19 @@ UIApplicationSupportsIndirectInputEvents + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + io.flutter.plugins.firebase.analytics.example + CFBundleURLSchemes + + ffa-e2e + + + UIApplicationSceneManifest UIApplicationSupportsMultipleScenes 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 287bb783a350..411c9ad7eab8 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 @@ -32,11 +32,22 @@ let kFLTFirebaseAnalyticsUserId = "userId" // swift-format-ignore: AlwaysUseLowerCamelCase let FLTFirebaseAnalyticsChannelName = "plugins.flutter.io/firebase_analytics" +/// Integration-test channel. Used by the example e2e suite to drive campaign +/// URL / universal-link handling against the live Analytics SDK. +// swift-format-ignore: AlwaysUseLowerCamelCase +let FLTFirebaseAnalyticsTestChannelName = "plugins.flutter.io/firebase_analytics_test" + extension FlutterError: Error {} public class FirebaseAnalyticsPlugin: NSObject, FLTFirebasePluginProtocol, FlutterPlugin, FirebaseAnalyticsHostApi { + #if os(iOS) + fileprivate var testChannel: FlutterMethodChannel? + fileprivate static var sceneDelegateRegistered = false + fileprivate static var lastHandledCampaignUrl: String? + #endif + public static func register(with registrar: any FlutterPluginRegistrar) { let binaryMessenger: FlutterBinaryMessenger @@ -50,6 +61,7 @@ public class FirebaseAnalyticsPlugin: NSObject, FLTFirebasePluginProtocol, Flutt FirebaseAnalyticsHostApiSetup.setUp(binaryMessenger: binaryMessenger, api: instance) #if os(iOS) registerSceneDelegateIfAvailable(registrar, instance: instance) + instance.registerCampaignTestChannel(registrar) #endif } @@ -317,6 +329,7 @@ public class FirebaseAnalyticsPlugin: NSObject, FLTFirebasePluginProtocol, Flutt return } _ = (registrar as AnyObject).perform(selector, with: instance) + FirebaseAnalyticsPlugin.sceneDelegateRegistered = true } /// Forwards UIScene URL and universal-link opens to Analytics. @@ -351,21 +364,124 @@ public class FirebaseAnalyticsPlugin: NSObject, FLTFirebasePluginProtocol, Flutt @objc(scene:continueUserActivity:) public func scene(_: UIScene, continue userActivity: NSUserActivity) -> Bool { - Analytics.handleUserActivity(userActivity) + handleCampaign(userActivity: userActivity) return false } + fileprivate func handleCampaign(url: URL) { + Self.lastHandledCampaignUrl = url.absoluteString + Analytics.handleOpen(url) + } + + fileprivate func handleCampaign(userActivity: NSUserActivity) { + if let url = userActivity.webpageURL { + Self.lastHandledCampaignUrl = url.absoluteString + } + Analytics.handleUserActivity(userActivity) + } + private func handleCampaign(urlContexts: Set?) { guard let urlContexts else { return } for context in urlContexts { - Analytics.handleOpen(context.url) + handleCampaign(url: context.url) } } private func handleCampaign(userActivities: Set?) { guard let userActivities else { return } for activity in userActivities { - Analytics.handleUserActivity(activity) + handleCampaign(userActivity: activity) + } + } + + fileprivate func registerCampaignTestChannel(_ registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel( + name: FLTFirebaseAnalyticsTestChannelName, + binaryMessenger: registrar.messenger() + ) + testChannel = channel + channel.setMethodCallHandler { [weak self] call, result in + guard let self else { + result( + FlutterError( + code: "unavailable", + message: "Plugin deallocated", + details: nil + ) + ) + return + } + self.handleTestChannel(call, result: result) + } + } + + private func handleTestChannel( + _ call: FlutterMethodCall, + result: @escaping FlutterResult + ) { + switch call.method { + case "isSceneDelegateRegistered": + result(Self.sceneDelegateRegistered) + case "getLastHandledCampaignUrl": + result(Self.lastHandledCampaignUrl) + case "handleCampaignUrl": + guard let urlString = call.arguments as? String, let url = URL(string: urlString) + else { + result( + FlutterError( + code: "invalid-argument", + message: "Expected a URL string", + details: nil + ) + ) + return + } + handleCampaign(url: url) + result(nil) + case "handleCampaignUserActivity": + guard let urlString = call.arguments as? String, let url = URL(string: urlString) + else { + result( + FlutterError( + code: "invalid-argument", + message: "Expected a URL string", + details: nil + ) + ) + return + } + guard let scene = UIApplication.shared.connectedScenes.first else { + result( + FlutterError( + code: "unavailable", + message: "No connected UIScene", + details: nil + ) + ) + return + } + let activity = NSUserActivity(activityType: NSUserActivityTypeBrowsingWeb) + activity.webpageURL = url + _ = self.scene(scene, continue: activity) + result(Self.lastHandledCampaignUrl) + case "openUrl": + guard let urlString = call.arguments as? String, let url = URL(string: urlString) + else { + result( + FlutterError( + code: "invalid-argument", + message: "Expected a URL string", + details: nil + ) + ) + return + } + Self.lastHandledCampaignUrl = nil + UIApplication.shared.open(url, options: [:]) { success in + result(success) + } + default: + result(FlutterMethodNotImplemented) } } } From 511749330a73d3928b538bc0076100f12865dd9e Mon Sep 17 00:00:00 2001 From: Jude Kwashie Date: Thu, 27 Aug 2026 12:20:26 +0000 Subject: [PATCH 4/6] chore: retrigger CI From c43fc7bd0ee681dd32df4f0db830868f5f98c4cf Mon Sep 17 00:00:00 2001 From: Jude Kwashie Date: Thu, 27 Aug 2026 12:27:31 +0000 Subject: [PATCH 5/6] fix(analytics,ios): drop redundant FlutterError Error conformance firebase_core already provides the retroactive conformance; redeclaring it fails CI on Flutter SDKs where FlutterError already conforms to Error. --- .../Sources/firebase_analytics/FirebaseAnalyticsPlugin.swift | 2 -- 1 file changed, 2 deletions(-) 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 411c9ad7eab8..b9ca290f3026 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 @@ -37,8 +37,6 @@ let FLTFirebaseAnalyticsChannelName = "plugins.flutter.io/firebase_analytics" // swift-format-ignore: AlwaysUseLowerCamelCase let FLTFirebaseAnalyticsTestChannelName = "plugins.flutter.io/firebase_analytics_test" -extension FlutterError: Error {} - public class FirebaseAnalyticsPlugin: NSObject, FLTFirebasePluginProtocol, FlutterPlugin, FirebaseAnalyticsHostApi { From 84b97ffe35343b5b331ebacb6032f4cd6b599e99 Mon Sep 17 00:00:00 2001 From: Jude Kwashie Date: Thu, 27 Aug 2026 14:54:16 +0000 Subject: [PATCH 6/6] test(analytics,ios): remove UIScene campaign e2e harness The test channel only echoed plugin state and could not observe firebase_campaign. Keep the scene-delegate forwarding, which was verified manually. --- .../example/integration_test/e2e_test.dart | 96 -------------- .../example/ios/Runner/Info.plist | 13 -- .../FirebaseAnalyticsPlugin.swift | 122 +----------------- 3 files changed, 3 insertions(+), 228 deletions(-) diff --git a/packages/firebase_analytics/firebase_analytics/example/integration_test/e2e_test.dart b/packages/firebase_analytics/firebase_analytics/example/integration_test/e2e_test.dart index e8dcbe5312eb..c8eb946c6e8c 100644 --- a/packages/firebase_analytics/firebase_analytics/example/integration_test/e2e_test.dart +++ b/packages/firebase_analytics/firebase_analytics/example/integration_test/e2e_test.dart @@ -433,101 +433,5 @@ void main() { defaultTargetPlatform != TargetPlatform.macOS), ); }); - - // firebase_campaign is recorded by the native SDK and is only visible in - // BigQuery (not DebugView), so CI cannot assert the exported event. These - // tests drive the same UIScene handlers the plugin registers and confirm - // Analytics.handleOpen / handleUserActivity run against the live project. - group( - 'UIScene campaign attribution', - () { - const channel = MethodChannel( - 'plugins.flutter.io/firebase_analytics_test', - ); - - String campaignUrl() { - final campaign = 'uiscene_${DateTime.now().microsecondsSinceEpoch}'; - return 'https://flutterfire.dev/?utm_source=flutterfire_e2e' - '&utm_medium=integration_test&utm_campaign=$campaign'; - } - - setUp(() async { - await FirebaseAnalytics.instance.setAnalyticsCollectionEnabled(true); - await FirebaseAnalytics.instance.setConsent( - analyticsStorageConsentGranted: true, - adStorageConsentGranted: true, - ); - }); - - test('registers as a Flutter scene delegate', () async { - final registered = - await channel.invokeMethod('isSceneDelegateRegistered'); - expect(registered, isTrue); - }); - - test( - 'handleOpen forwards a campaign URL to the live Analytics SDK', - () async { - final url = campaignUrl(); - await channel.invokeMethod('handleCampaignUrl', url); - - final handled = await channel.invokeMethod( - 'getLastHandledCampaignUrl', - ); - expect(handled, url); - - await expectLater( - FirebaseAnalytics.instance.logEvent( - name: 'uiscene_campaign_followup', - parameters: {'source': 'e2e'}, - ), - completes, - ); - }, - ); - - test( - 'scene continueUserActivity forwards a universal link', - () async { - final url = campaignUrl(); - final handled = await channel.invokeMethod( - 'handleCampaignUserActivity', - url, - ); - expect(handled, url); - }, - ); - - test( - 'scene openURLContexts receives a campaign URL opened on the live app', - () async { - final campaign = 'uiscene_${DateTime.now().microsecondsSinceEpoch}'; - final url = - 'ffa-e2e://campaign?utm_source=flutterfire_e2e&utm_medium=integration_test&utm_campaign=$campaign'; - - final opened = await channel.invokeMethod('openUrl', url); - expect(opened, isTrue); - - String? handled; - for (var i = 0; i < 25; i++) { - handled = await channel.invokeMethod( - 'getLastHandledCampaignUrl', - ); - if (handled == url) { - break; - } - await Future.delayed(const Duration(milliseconds: 200)); - } - expect( - handled, - url, - reason: - 'UIScene should deliver the custom-scheme campaign URL to the plugin', - ); - }, - ); - }, - skip: kIsWeb || defaultTargetPlatform != TargetPlatform.iOS, - ); }); } diff --git a/packages/firebase_analytics/firebase_analytics/example/ios/Runner/Info.plist b/packages/firebase_analytics/firebase_analytics/example/ios/Runner/Info.plist index afff5b105a16..248bf3883db1 100755 --- a/packages/firebase_analytics/firebase_analytics/example/ios/Runner/Info.plist +++ b/packages/firebase_analytics/firebase_analytics/example/ios/Runner/Info.plist @@ -49,19 +49,6 @@ UIApplicationSupportsIndirectInputEvents - CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLName - io.flutter.plugins.firebase.analytics.example - CFBundleURLSchemes - - ffa-e2e - - - UIApplicationSceneManifest UIApplicationSupportsMultipleScenes 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 b9ca290f3026..23b7ca781dc7 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 @@ -32,20 +32,9 @@ let kFLTFirebaseAnalyticsUserId = "userId" // swift-format-ignore: AlwaysUseLowerCamelCase let FLTFirebaseAnalyticsChannelName = "plugins.flutter.io/firebase_analytics" -/// Integration-test channel. Used by the example e2e suite to drive campaign -/// URL / universal-link handling against the live Analytics SDK. -// swift-format-ignore: AlwaysUseLowerCamelCase -let FLTFirebaseAnalyticsTestChannelName = "plugins.flutter.io/firebase_analytics_test" - public class FirebaseAnalyticsPlugin: NSObject, FLTFirebasePluginProtocol, FlutterPlugin, FirebaseAnalyticsHostApi { - #if os(iOS) - fileprivate var testChannel: FlutterMethodChannel? - fileprivate static var sceneDelegateRegistered = false - fileprivate static var lastHandledCampaignUrl: String? - #endif - public static func register(with registrar: any FlutterPluginRegistrar) { let binaryMessenger: FlutterBinaryMessenger @@ -59,7 +48,6 @@ public class FirebaseAnalyticsPlugin: NSObject, FLTFirebasePluginProtocol, Flutt FirebaseAnalyticsHostApiSetup.setUp(binaryMessenger: binaryMessenger, api: instance) #if os(iOS) registerSceneDelegateIfAvailable(registrar, instance: instance) - instance.registerCampaignTestChannel(registrar) #endif } @@ -327,7 +315,6 @@ public class FirebaseAnalyticsPlugin: NSObject, FLTFirebasePluginProtocol, Flutt return } _ = (registrar as AnyObject).perform(selector, with: instance) - FirebaseAnalyticsPlugin.sceneDelegateRegistered = true } /// Forwards UIScene URL and universal-link opens to Analytics. @@ -362,124 +349,21 @@ public class FirebaseAnalyticsPlugin: NSObject, FLTFirebasePluginProtocol, Flutt @objc(scene:continueUserActivity:) public func scene(_: UIScene, continue userActivity: NSUserActivity) -> Bool { - handleCampaign(userActivity: userActivity) - return false - } - - fileprivate func handleCampaign(url: URL) { - Self.lastHandledCampaignUrl = url.absoluteString - Analytics.handleOpen(url) - } - - fileprivate func handleCampaign(userActivity: NSUserActivity) { - if let url = userActivity.webpageURL { - Self.lastHandledCampaignUrl = url.absoluteString - } Analytics.handleUserActivity(userActivity) + return false } private func handleCampaign(urlContexts: Set?) { guard let urlContexts else { return } for context in urlContexts { - handleCampaign(url: context.url) + Analytics.handleOpen(context.url) } } private func handleCampaign(userActivities: Set?) { guard let userActivities else { return } for activity in userActivities { - handleCampaign(userActivity: activity) - } - } - - fileprivate func registerCampaignTestChannel(_ registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel( - name: FLTFirebaseAnalyticsTestChannelName, - binaryMessenger: registrar.messenger() - ) - testChannel = channel - channel.setMethodCallHandler { [weak self] call, result in - guard let self else { - result( - FlutterError( - code: "unavailable", - message: "Plugin deallocated", - details: nil - ) - ) - return - } - self.handleTestChannel(call, result: result) - } - } - - private func handleTestChannel( - _ call: FlutterMethodCall, - result: @escaping FlutterResult - ) { - switch call.method { - case "isSceneDelegateRegistered": - result(Self.sceneDelegateRegistered) - case "getLastHandledCampaignUrl": - result(Self.lastHandledCampaignUrl) - case "handleCampaignUrl": - guard let urlString = call.arguments as? String, let url = URL(string: urlString) - else { - result( - FlutterError( - code: "invalid-argument", - message: "Expected a URL string", - details: nil - ) - ) - return - } - handleCampaign(url: url) - result(nil) - case "handleCampaignUserActivity": - guard let urlString = call.arguments as? String, let url = URL(string: urlString) - else { - result( - FlutterError( - code: "invalid-argument", - message: "Expected a URL string", - details: nil - ) - ) - return - } - guard let scene = UIApplication.shared.connectedScenes.first else { - result( - FlutterError( - code: "unavailable", - message: "No connected UIScene", - details: nil - ) - ) - return - } - let activity = NSUserActivity(activityType: NSUserActivityTypeBrowsingWeb) - activity.webpageURL = url - _ = self.scene(scene, continue: activity) - result(Self.lastHandledCampaignUrl) - case "openUrl": - guard let urlString = call.arguments as? String, let url = URL(string: urlString) - else { - result( - FlutterError( - code: "invalid-argument", - message: "Expected a URL string", - details: nil - ) - ) - return - } - Self.lastHandledCampaignUrl = nil - UIApplication.shared.open(url, options: [:]) { success in - result(success) - } - default: - result(FlutterMethodNotImplemented) + Analytics.handleUserActivity(activity) } } }