From a558eb9166d5f52c8ed4d60f51d5f4e1ba750c3d Mon Sep 17 00:00:00 2001 From: Jude Kwashie Date: Wed, 4 Feb 2026 13:05:46 +0000 Subject: [PATCH 01/13] feat(analytics, iOS): add support for `logTransaction` --- .../GeneratedAndroidFirebaseAnalytics.g.kt | 20 +++++++++ .../FirebaseAnalyticsMessages.g.swift | 22 ++++++++++ .../FirebaseAnalyticsPlugin.swift | 42 +++++++++++++++++++ .../lib/src/firebase_analytics.dart | 9 ++++ .../firebase_analytics/windows/messages.g.cpp | 39 +++++++++++++++++ .../firebase_analytics/windows/messages.g.h | 3 ++ .../method_channel_firebase_analytics.dart | 11 +++++ .../lib/src/pigeon/messages.pigeon.dart | 26 ++++++++++++ ...platform_interface_firebase_analytics.dart | 6 +++ .../pigeons/messages.dart | 3 ++ .../test/pigeon/test_api.dart | 34 +++++++++++++++ 11 files changed, 215 insertions(+) diff --git a/packages/firebase_analytics/firebase_analytics/android/src/main/kotlin/io/flutter/plugins/firebase/analytics/GeneratedAndroidFirebaseAnalytics.g.kt b/packages/firebase_analytics/firebase_analytics/android/src/main/kotlin/io/flutter/plugins/firebase/analytics/GeneratedAndroidFirebaseAnalytics.g.kt index 0a8351824439..fdfc88dc8f42 100644 --- a/packages/firebase_analytics/firebase_analytics/android/src/main/kotlin/io/flutter/plugins/firebase/analytics/GeneratedAndroidFirebaseAnalytics.g.kt +++ b/packages/firebase_analytics/firebase_analytics/android/src/main/kotlin/io/flutter/plugins/firebase/analytics/GeneratedAndroidFirebaseAnalytics.g.kt @@ -147,6 +147,7 @@ interface FirebaseAnalyticsHostApi { fun getAppInstanceId(callback: (Result) -> Unit) fun getSessionId(callback: (Result) -> Unit) fun initiateOnDeviceConversionMeasurement(arguments: Map, callback: (Result) -> Unit) + fun logTransaction(transactionId: String, callback: (Result) -> Unit) companion object { /** The codec used by FirebaseAnalyticsHostApi. */ @@ -363,6 +364,25 @@ interface FirebaseAnalyticsHostApi { channel.setMessageHandler(null) } } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.firebase_analytics_platform_interface.FirebaseAnalyticsHostApi.logTransaction$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val transactionIdArg = args[0] as String + api.logTransaction(transactionIdArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(GeneratedAndroidFirebaseAnalyticsPigeonUtils.wrapError(error)) + } else { + reply.reply(GeneratedAndroidFirebaseAnalyticsPigeonUtils.wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } } } } diff --git a/packages/firebase_analytics/firebase_analytics/ios/firebase_analytics/Sources/firebase_analytics/FirebaseAnalyticsMessages.g.swift b/packages/firebase_analytics/firebase_analytics/ios/firebase_analytics/Sources/firebase_analytics/FirebaseAnalyticsMessages.g.swift index f8124ea1dc43..d8b175ba6e20 100644 --- a/packages/firebase_analytics/firebase_analytics/ios/firebase_analytics/Sources/firebase_analytics/FirebaseAnalyticsMessages.g.swift +++ b/packages/firebase_analytics/firebase_analytics/ios/firebase_analytics/Sources/firebase_analytics/FirebaseAnalyticsMessages.g.swift @@ -220,6 +220,7 @@ protocol FirebaseAnalyticsHostApi { func getSessionId(completion: @escaping (Result) -> Void) func initiateOnDeviceConversionMeasurement(arguments: [String: String?], completion: @escaping (Result) -> Void) + func logTransaction(transactionId: String, completion: @escaping (Result) -> Void) } /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. @@ -459,5 +460,26 @@ class FirebaseAnalyticsHostApiSetup { } else { initiateOnDeviceConversionMeasurementChannel.setMessageHandler(nil) } + let logTransactionChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.firebase_analytics_platform_interface.FirebaseAnalyticsHostApi.logTransaction\(channelSuffix)", + binaryMessenger: binaryMessenger, + codec: codec + ) + if let api { + logTransactionChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let transactionIdArg = args[0] as! String + api.logTransaction(transactionId: transactionIdArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case let .failure(error): + reply(wrapError(error)) + } + } + } + } else { + logTransactionChannel.setMessageHandler(nil) + } } } 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 fcd6bda21819..fdc68533e07d 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 @@ -14,6 +14,7 @@ import firebase_core_shared #endif import FirebaseAnalytics +import StoreKit let kFLTFirebaseAnalyticsName = "name" let kFLTFirebaseAnalyticsValue = "value" @@ -28,6 +29,8 @@ let kFLTFirebaseAnalyticsUserId = "userId" let FLTFirebaseAnalyticsChannelName = "plugins.flutter.io/firebase_analytics" +extension FlutterError: Error {} + public class FirebaseAnalyticsPlugin: NSObject, FLTFirebasePluginProtocol, FlutterPlugin, FirebaseAnalyticsHostApi { public static func register(with registrar: any FlutterPluginRegistrar) { @@ -142,6 +145,45 @@ public class FirebaseAnalyticsPlugin: NSObject, FLTFirebasePluginProtocol, Flutt completion(.success(())) } + func logTransaction(transactionId: String, + completion: @escaping (Result) -> Void) { + Task { + do { + guard let id = UInt64(transactionId) else { + completion(.failure(FlutterError( + code: "firebase_analytics", + message: "Invalid transactionId", + details: nil + ))) + return + } + let transaction = try await fetchTransaction( + by: UInt64(id) + ) + + Analytics.logTransaction(transaction!) + + completion(.success(())) + } catch { + completion(.failure(error)) + } + } + } + + private func fetchTransaction(by id: UInt64) async throws -> Transaction? { + for await result in Transaction.all { + switch result { + case let .verified(transaction): + if transaction.id == id { + return transaction + } + case .unverified: + continue + } + } + return nil + } + public func didReinitializeFirebaseCore(_ completion: @escaping () -> Void) { completion() } diff --git a/packages/firebase_analytics/firebase_analytics/lib/src/firebase_analytics.dart b/packages/firebase_analytics/firebase_analytics/lib/src/firebase_analytics.dart index 237a972b19c7..eac1d37f3d77 100755 --- a/packages/firebase_analytics/firebase_analytics/lib/src/firebase_analytics.dart +++ b/packages/firebase_analytics/firebase_analytics/lib/src/firebase_analytics.dart @@ -1242,6 +1242,15 @@ class FirebaseAnalytics extends FirebasePluginPlatform { ); } + /// Logs verified in-app purchase events in Google Analytics for Firebase + /// after a purchase is successful. + Future logTransaction(String transactionId) async { + if (defaultTargetPlatform != TargetPlatform.iOS) { + throw UnimplementedError('logTransaction() is only supported on iOS.'); + } + return _delegate.logTransaction(transactionId: transactionId); + } + /// Sets the duration of inactivity that terminates the current session. /// /// The default value is 1800000 milliseconds (30 minutes). diff --git a/packages/firebase_analytics/firebase_analytics/windows/messages.g.cpp b/packages/firebase_analytics/firebase_analytics/windows/messages.g.cpp index 2402f854a82a..14a92edf1452 100644 --- a/packages/firebase_analytics/firebase_analytics/windows/messages.g.cpp +++ b/packages/firebase_analytics/firebase_analytics/windows/messages.g.cpp @@ -525,6 +525,45 @@ void FirebaseAnalyticsHostApi::SetUp( channel.SetMessageHandler(nullptr); } } + { + BasicMessageChannel<> channel( + binary_messenger, + "dev.flutter.pigeon.firebase_analytics_platform_interface." + "FirebaseAnalyticsHostApi.logTransaction" + + prepended_suffix, + &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_transaction_id_arg = args.at(0); + if (encodable_transaction_id_arg.IsNull()) { + reply(WrapError("transaction_id_arg unexpectedly null.")); + return; + } + const auto& transaction_id_arg = + std::get(encodable_transaction_id_arg); + api->LogTransaction( + transaction_id_arg, + [reply](std::optional&& output) { + if (output.has_value()) { + reply(WrapError(output.value())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue()); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } } EncodableValue FirebaseAnalyticsHostApi::WrapError( diff --git a/packages/firebase_analytics/firebase_analytics/windows/messages.g.h b/packages/firebase_analytics/firebase_analytics/windows/messages.g.h index 69c859c6e515..b755d9ef6045 100644 --- a/packages/firebase_analytics/firebase_analytics/windows/messages.g.h +++ b/packages/firebase_analytics/firebase_analytics/windows/messages.g.h @@ -138,6 +138,9 @@ class FirebaseAnalyticsHostApi { virtual void InitiateOnDeviceConversionMeasurement( const flutter::EncodableMap& arguments, std::function reply)> result) = 0; + virtual void LogTransaction( + const std::string& transaction_id, + std::function reply)> result) = 0; // The codec used by FirebaseAnalyticsHostApi. static const flutter::StandardMessageCodec& GetCodec(); diff --git a/packages/firebase_analytics/firebase_analytics_platform_interface/lib/src/method_channel/method_channel_firebase_analytics.dart b/packages/firebase_analytics/firebase_analytics_platform_interface/lib/src/method_channel/method_channel_firebase_analytics.dart index 0ad8119fe21c..e3c5d41eaa42 100644 --- a/packages/firebase_analytics/firebase_analytics_platform_interface/lib/src/method_channel/method_channel_firebase_analytics.dart +++ b/packages/firebase_analytics/firebase_analytics_platform_interface/lib/src/method_channel/method_channel_firebase_analytics.dart @@ -195,4 +195,15 @@ class MethodChannelFirebaseAnalytics extends FirebaseAnalyticsPlatform { convertPlatformException(e, s); } } + + @override + Future logTransaction({ + required String transactionId, + }) { + try { + return _api.logTransaction(transactionId); + } catch (e, s) { + convertPlatformException(e, s); + } + } } diff --git a/packages/firebase_analytics/firebase_analytics_platform_interface/lib/src/pigeon/messages.pigeon.dart b/packages/firebase_analytics/firebase_analytics_platform_interface/lib/src/pigeon/messages.pigeon.dart index 6b74cc466a3d..003dd773639e 100644 --- a/packages/firebase_analytics/firebase_analytics_platform_interface/lib/src/pigeon/messages.pigeon.dart +++ b/packages/firebase_analytics/firebase_analytics_platform_interface/lib/src/pigeon/messages.pigeon.dart @@ -416,4 +416,30 @@ class FirebaseAnalyticsHostApi { return; } } + + Future logTransaction(String transactionId) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.firebase_analytics_platform_interface.FirebaseAnalyticsHostApi.logTransaction$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = + pigeonVar_channel.send([transactionId]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } } diff --git a/packages/firebase_analytics/firebase_analytics_platform_interface/lib/src/platform_interface/platform_interface_firebase_analytics.dart b/packages/firebase_analytics/firebase_analytics_platform_interface/lib/src/platform_interface/platform_interface_firebase_analytics.dart index 8fbe90066d5b..f69c816e707d 100644 --- a/packages/firebase_analytics/firebase_analytics_platform_interface/lib/src/platform_interface/platform_interface_firebase_analytics.dart +++ b/packages/firebase_analytics/firebase_analytics_platform_interface/lib/src/platform_interface/platform_interface_firebase_analytics.dart @@ -209,4 +209,10 @@ abstract class FirebaseAnalyticsPlatform extends PlatformInterface { 'initiateOnDeviceConversionMeasurement() is not implemented', ); } + + Future logTransaction({ + required String transactionId, + }) { + throw UnimplementedError('logTransaction() is not implemented'); + } } diff --git a/packages/firebase_analytics/firebase_analytics_platform_interface/pigeons/messages.dart b/packages/firebase_analytics/firebase_analytics_platform_interface/pigeons/messages.dart index 4c1ada746e85..b7008b4d3871 100644 --- a/packages/firebase_analytics/firebase_analytics_platform_interface/pigeons/messages.dart +++ b/packages/firebase_analytics/firebase_analytics_platform_interface/pigeons/messages.dart @@ -66,4 +66,7 @@ abstract class FirebaseAnalyticsHostApi { @async void initiateOnDeviceConversionMeasurement(Map arguments); + + @async + void logTransaction(String transactionId); } diff --git a/packages/firebase_analytics/firebase_analytics_platform_interface/test/pigeon/test_api.dart b/packages/firebase_analytics/firebase_analytics_platform_interface/test/pigeon/test_api.dart index cbef0d67a9eb..591cce9f19e2 100644 --- a/packages/firebase_analytics/firebase_analytics_platform_interface/test/pigeon/test_api.dart +++ b/packages/firebase_analytics/firebase_analytics_platform_interface/test/pigeon/test_api.dart @@ -67,6 +67,8 @@ abstract class TestFirebaseAnalyticsHostApi { Future initiateOnDeviceConversionMeasurement( Map arguments); + Future logTransaction(String transactionId); + static void setUp( TestFirebaseAnalyticsHostApi? api, { BinaryMessenger? binaryMessenger, @@ -409,5 +411,37 @@ abstract class TestFirebaseAnalyticsHostApi { }); } } + { + final BasicMessageChannel< + Object?> pigeonVar_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.firebase_analytics_platform_interface.FirebaseAnalyticsHostApi.logTransaction$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(pigeonVar_channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(pigeonVar_channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.firebase_analytics_platform_interface.FirebaseAnalyticsHostApi.logTransaction was null.'); + final List args = (message as List?)!; + final String? arg_transactionId = (args[0] as String?); + assert(arg_transactionId != null, + 'Argument for dev.flutter.pigeon.firebase_analytics_platform_interface.FirebaseAnalyticsHostApi.logTransaction was null, expected non-null String.'); + try { + await api.logTransaction(arg_transactionId!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } } } From 9cb876c0cfabbebe43fceb9abcdff4c1b339a70d Mon Sep 17 00:00:00 2001 From: Jude Kwashie Date: Wed, 4 Feb 2026 13:28:32 +0000 Subject: [PATCH 02/13] chore: add test for `logTransaction` method --- .../firebase_analytics_e2e_test.dart | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/integration_test/firebase_analytics/firebase_analytics_e2e_test.dart b/tests/integration_test/firebase_analytics/firebase_analytics_e2e_test.dart index f7d309a3b306..de87c90a1594 100644 --- a/tests/integration_test/firebase_analytics/firebase_analytics_e2e_test.dart +++ b/tests/integration_test/firebase_analytics/firebase_analytics_e2e_test.dart @@ -356,5 +356,16 @@ void main() { }, skip: kIsWeb || defaultTargetPlatform != TargetPlatform.iOS, ); + + test( + 'logTransaction', + () async { + await expectLater( + FirebaseAnalytics.instance.logTransaction('12345'), + completes, + ); + }, + skip: defaultTargetPlatform != TargetPlatform.iOS, + ); }); } From 1d06c010381254896658f04ea80332f4b6b8cc89 Mon Sep 17 00:00:00 2001 From: Jude Kwashie Date: Thu, 5 Feb 2026 09:28:53 +0000 Subject: [PATCH 03/13] chore: update logTransaction to require iOS 15+ or macOS 12+ --- .../FirebaseAnalyticsPlugin.swift | 44 +++++++++++-------- .../lib/src/firebase_analytics.dart | 5 +++ 2 files changed, 31 insertions(+), 18 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 fdc68533e07d..2c7f9ee2f6bd 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 @@ -147,26 +147,34 @@ public class FirebaseAnalyticsPlugin: NSObject, FLTFirebasePluginProtocol, Flutt func logTransaction(transactionId: String, completion: @escaping (Result) -> Void) { - Task { - do { - guard let id = UInt64(transactionId) else { - completion(.failure(FlutterError( - code: "firebase_analytics", - message: "Invalid transactionId", - details: nil - ))) - return + if #available(iOS 15.0, macOS 12.0, *) { + Task { + do { + guard let id = UInt64(transactionId) else { + completion(.failure(FlutterError( + code: "firebase_analytics", + message: "Invalid transactionId", + details: nil + ))) + return + } + let transaction = try await fetchTransaction( + by: UInt64(id) + ) + + Analytics.logTransaction(transaction!) + + completion(.success(())) + } catch { + completion(.failure(error)) } - let transaction = try await fetchTransaction( - by: UInt64(id) - ) - - Analytics.logTransaction(transaction!) - - completion(.success(())) - } catch { - completion(.failure(error)) } + } else { + completion(.failure(FlutterError( + code: "firebase_analytics", + message: "logTransaction requires iOS 15+ or macOS 12+", + details: nil + ))) } } diff --git a/packages/firebase_analytics/firebase_analytics/lib/src/firebase_analytics.dart b/packages/firebase_analytics/firebase_analytics/lib/src/firebase_analytics.dart index eac1d37f3d77..eb324a0ff8dd 100755 --- a/packages/firebase_analytics/firebase_analytics/lib/src/firebase_analytics.dart +++ b/packages/firebase_analytics/firebase_analytics/lib/src/firebase_analytics.dart @@ -1244,6 +1244,11 @@ class FirebaseAnalytics extends FirebasePluginPlatform { /// Logs verified in-app purchase events in Google Analytics for Firebase /// after a purchase is successful. + /// + /// Only available on iOS. + /// + /// You can obtain the [transactionId] from the + /// [in_app_purchase](https://pub.dev/packages/in_app_purchase) package. Future logTransaction(String transactionId) async { if (defaultTargetPlatform != TargetPlatform.iOS) { throw UnimplementedError('logTransaction() is only supported on iOS.'); From 971e9123964121ed7066efacade02d4a3d8d7f4e Mon Sep 17 00:00:00 2001 From: Jude Kwashie Date: Thu, 5 Feb 2026 09:49:25 +0000 Subject: [PATCH 04/13] chore: update logTransaction to require iOS 15+ or macOS 12+ --- .../firebase_analytics/FirebaseAnalyticsPlugin.swift | 8 +------- 1 file changed, 1 insertion(+), 7 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 2c7f9ee2f6bd..6e108c031d39 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 @@ -145,9 +145,9 @@ public class FirebaseAnalyticsPlugin: NSObject, FLTFirebasePluginProtocol, Flutt completion(.success(())) } + @available(iOS 15.0, macOS 12.0, *) func logTransaction(transactionId: String, completion: @escaping (Result) -> Void) { - if #available(iOS 15.0, macOS 12.0, *) { Task { do { guard let id = UInt64(transactionId) else { @@ -169,12 +169,6 @@ public class FirebaseAnalyticsPlugin: NSObject, FLTFirebasePluginProtocol, Flutt completion(.failure(error)) } } - } else { - completion(.failure(FlutterError( - code: "firebase_analytics", - message: "logTransaction requires iOS 15+ or macOS 12+", - details: nil - ))) } } From 2e7a65762eddf309650ffbce3413641271920667 Mon Sep 17 00:00:00 2001 From: Jude Kwashie Date: Thu, 5 Feb 2026 09:50:53 +0000 Subject: [PATCH 05/13] chore: fix formatting --- .../FirebaseAnalyticsPlugin.swift | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 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 6e108c031d39..6f15d89eeb43 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 @@ -148,26 +148,25 @@ public class FirebaseAnalyticsPlugin: NSObject, FLTFirebasePluginProtocol, Flutt @available(iOS 15.0, macOS 12.0, *) func logTransaction(transactionId: String, completion: @escaping (Result) -> Void) { - Task { - do { - guard let id = UInt64(transactionId) else { - completion(.failure(FlutterError( - code: "firebase_analytics", - message: "Invalid transactionId", - details: nil - ))) - return - } - let transaction = try await fetchTransaction( - by: UInt64(id) - ) - - Analytics.logTransaction(transaction!) - - completion(.success(())) - } catch { - completion(.failure(error)) + Task { + do { + guard let id = UInt64(transactionId) else { + completion(.failure(FlutterError( + code: "firebase_analytics", + message: "Invalid transactionId", + details: nil + ))) + return } + let transaction = try await fetchTransaction( + by: UInt64(id) + ) + + Analytics.logTransaction(transaction!) + + completion(.success(())) + } catch { + completion(.failure(error)) } } } From b701f2da0410e6a3c3c1899df6dcda216803c260 Mon Sep 17 00:00:00 2001 From: Jude Kwashie Date: Thu, 5 Feb 2026 10:02:15 +0000 Subject: [PATCH 06/13] chore: add availability check for fetchTransaction method on iOS 15+ and macOS 12+ --- .../Sources/firebase_analytics/FirebaseAnalyticsPlugin.swift | 1 + 1 file changed, 1 insertion(+) 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 6f15d89eeb43..b92e8e8c7410 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 @@ -171,6 +171,7 @@ public class FirebaseAnalyticsPlugin: NSObject, FLTFirebasePluginProtocol, Flutt } } + @available(iOS 15.0, macOS 12.0, *) private func fetchTransaction(by id: UInt64) async throws -> Transaction? { for await result in Transaction.all { switch result { From 3832901177c712b3740b0f0f548b1bd5ef38a162 Mon Sep 17 00:00:00 2001 From: Jude Kwashie Date: Thu, 5 Feb 2026 10:46:47 +0000 Subject: [PATCH 07/13] chore: enhance logTransaction method with platform-specific availability checks and improved transaction retrieval --- .../FirebaseAnalyticsPlugin.swift | 72 +++++++++++++------ 1 file changed, 52 insertions(+), 20 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 b92e8e8c7410..0a5ee2378f74 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 @@ -145,9 +145,38 @@ public class FirebaseAnalyticsPlugin: NSObject, FLTFirebasePluginProtocol, Flutt completion(.success(())) } - @available(iOS 15.0, macOS 12.0, *) func logTransaction(transactionId: String, completion: @escaping (Result) -> Void) { + #if os(macOS) + if #available(macOS 12.0, *) { + logTransactionWithStoreKit(transactionId: transactionId, completion: completion) + } else { + completion(.failure(FlutterError( + code: "firebase_analytics", + message: "logTransaction() is only supported on macOS 12.0 or newer", + details: nil + ))) + } + #else + if #available(iOS 15.0, *) { + logTransactionWithStoreKit(transactionId: transactionId, completion: completion) + } else { + completion(.failure(FlutterError( + code: "firebase_analytics", + message: "logTransaction() is only supported on iOS 15.0 or newer", + details: nil + ))) + } + #endif + } + + #if os(macOS) + @available(macOS 12.0, *) + #else + @available(iOS 15.0, *) + #endif + private func logTransactionWithStoreKit(transactionId: String, + completion: @escaping (Result) -> Void) { Task { do { guard let id = UInt64(transactionId) else { @@ -158,12 +187,30 @@ public class FirebaseAnalyticsPlugin: NSObject, FLTFirebasePluginProtocol, Flutt ))) return } - let transaction = try await fetchTransaction( - by: UInt64(id) - ) - Analytics.logTransaction(transaction!) + var foundTransaction: Transaction? + for await result in Transaction.all { + switch result { + case let .verified(transaction): + if transaction.id == id { + foundTransaction = transaction + break + } + case .unverified: + continue + } + } + guard let transaction = foundTransaction else { + completion(.failure(FlutterError( + code: "firebase_analytics", + message: "Transaction not found", + details: nil + ))) + return + } + + Analytics.logTransaction(transaction) completion(.success(())) } catch { completion(.failure(error)) @@ -171,21 +218,6 @@ public class FirebaseAnalyticsPlugin: NSObject, FLTFirebasePluginProtocol, Flutt } } - @available(iOS 15.0, macOS 12.0, *) - private func fetchTransaction(by id: UInt64) async throws -> Transaction? { - for await result in Transaction.all { - switch result { - case let .verified(transaction): - if transaction.id == id { - return transaction - } - case .unverified: - continue - } - } - return nil - } - public func didReinitializeFirebaseCore(_ completion: @escaping () -> Void) { completion() } From d35a37c9756986f4f42e803c6e3b9967449fdc59 Mon Sep 17 00:00:00 2001 From: Jude Kwashie Date: Thu, 5 Feb 2026 11:03:44 +0000 Subject: [PATCH 08/13] chore: update test skip condition to include macOS platform --- .../firebase_analytics/firebase_analytics_e2e_test.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/integration_test/firebase_analytics/firebase_analytics_e2e_test.dart b/tests/integration_test/firebase_analytics/firebase_analytics_e2e_test.dart index de87c90a1594..f4282d971e75 100644 --- a/tests/integration_test/firebase_analytics/firebase_analytics_e2e_test.dart +++ b/tests/integration_test/firebase_analytics/firebase_analytics_e2e_test.dart @@ -365,7 +365,8 @@ void main() { completes, ); }, - skip: defaultTargetPlatform != TargetPlatform.iOS, + skip: defaultTargetPlatform != TargetPlatform.iOS && + defaultTargetPlatform != TargetPlatform.macOS, ); }); } From d0ca2370e00fd88c7aeef868ae6dafd30a15f48d Mon Sep 17 00:00:00 2001 From: Jude Kwashie Date: Thu, 5 Feb 2026 11:27:39 +0000 Subject: [PATCH 09/13] chore: update logTransaction method to support macOS in addition to iOS --- .../firebase_analytics/lib/src/firebase_analytics.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/firebase_analytics/firebase_analytics/lib/src/firebase_analytics.dart b/packages/firebase_analytics/firebase_analytics/lib/src/firebase_analytics.dart index eb324a0ff8dd..e5251409d94e 100755 --- a/packages/firebase_analytics/firebase_analytics/lib/src/firebase_analytics.dart +++ b/packages/firebase_analytics/firebase_analytics/lib/src/firebase_analytics.dart @@ -1250,8 +1250,10 @@ class FirebaseAnalytics extends FirebasePluginPlatform { /// You can obtain the [transactionId] from the /// [in_app_purchase](https://pub.dev/packages/in_app_purchase) package. Future logTransaction(String transactionId) async { - if (defaultTargetPlatform != TargetPlatform.iOS) { - throw UnimplementedError('logTransaction() is only supported on iOS.'); + if (defaultTargetPlatform != TargetPlatform.iOS && + defaultTargetPlatform != TargetPlatform.macOS) { + throw UnimplementedError( + 'logTransaction() is only supported on iOS and macOS.'); } return _delegate.logTransaction(transactionId: transactionId); } From 43dab24bc419b91b7379824785d194c0742bf33b Mon Sep 17 00:00:00 2001 From: Jude Kwashie Date: Thu, 5 Feb 2026 14:47:43 +0000 Subject: [PATCH 10/13] chore: enhance logTransaction tests with error handling for invalid transactionId and missing transactions --- .../lib/src/firebase_analytics.dart | 3 +- .../firebase_analytics_e2e_test.dart | 49 ++++++++++++++----- 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/packages/firebase_analytics/firebase_analytics/lib/src/firebase_analytics.dart b/packages/firebase_analytics/firebase_analytics/lib/src/firebase_analytics.dart index e5251409d94e..38c5a9077977 100755 --- a/packages/firebase_analytics/firebase_analytics/lib/src/firebase_analytics.dart +++ b/packages/firebase_analytics/firebase_analytics/lib/src/firebase_analytics.dart @@ -1253,7 +1253,8 @@ class FirebaseAnalytics extends FirebasePluginPlatform { if (defaultTargetPlatform != TargetPlatform.iOS && defaultTargetPlatform != TargetPlatform.macOS) { throw UnimplementedError( - 'logTransaction() is only supported on iOS and macOS.'); + 'logTransaction() is only supported on iOS and macOS.', + ); } return _delegate.logTransaction(transactionId: transactionId); } diff --git a/tests/integration_test/firebase_analytics/firebase_analytics_e2e_test.dart b/tests/integration_test/firebase_analytics/firebase_analytics_e2e_test.dart index f4282d971e75..b446d6c87193 100644 --- a/tests/integration_test/firebase_analytics/firebase_analytics_e2e_test.dart +++ b/tests/integration_test/firebase_analytics/firebase_analytics_e2e_test.dart @@ -5,6 +5,7 @@ import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:tests/firebase_options.dart'; @@ -357,16 +358,42 @@ void main() { skip: kIsWeb || defaultTargetPlatform != TargetPlatform.iOS, ); - test( - 'logTransaction', - () async { - await expectLater( - FirebaseAnalytics.instance.logTransaction('12345'), - completes, - ); - }, - skip: defaultTargetPlatform != TargetPlatform.iOS && - defaultTargetPlatform != TargetPlatform.macOS, - ); + group('logTransaction', () { + test( + 'throws when transactionId is not a valid numeric string', + () async { + await expectLater( + FirebaseAnalytics.instance.logTransaction('not_a_number'), + throwsA( + isA().having( + (e) => e.message, + 'message', + 'Invalid transactionId', + ), + ), + ); + }, + skip: defaultTargetPlatform != TargetPlatform.iOS && + defaultTargetPlatform != TargetPlatform.macOS, + ); + + test( + 'throws when transactionId is valid format but transaction not found in StoreKit', + () async { + await expectLater( + FirebaseAnalytics.instance.logTransaction('12345'), + throwsA( + isA().having( + (e) => e.message, + 'message', + 'Transaction not found', + ), + ), + ); + }, + skip: defaultTargetPlatform != TargetPlatform.iOS && + defaultTargetPlatform != TargetPlatform.macOS, + ); + }); }); } From 68180831c2b4473b60e0a2153978e8554af423d0 Mon Sep 17 00:00:00 2001 From: Jude Kwashie Date: Fri, 6 Mar 2026 10:20:16 +0000 Subject: [PATCH 11/13] chore: update analytics example app to include manual test for `logTransaction` --- .../firebase_analytics/example/lib/main.dart | 80 +++++++++++++++++++ .../firebase_analytics/example/pubspec.yaml | 1 + .../analytics_storekit_config.storekit | 54 +++++++++++++ 3 files changed, 135 insertions(+) create mode 100644 packages/firebase_analytics/firebase_analytics/ios/firebase_analytics/analytics_storekit_config.storekit diff --git a/packages/firebase_analytics/firebase_analytics/example/lib/main.dart b/packages/firebase_analytics/firebase_analytics/example/lib/main.dart index 2b885ed94b1f..37c7dd8eec76 100755 --- a/packages/firebase_analytics/firebase_analytics/example/lib/main.dart +++ b/packages/firebase_analytics/firebase_analytics/example/lib/main.dart @@ -8,6 +8,7 @@ import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:in_app_purchase/in_app_purchase.dart'; import 'firebase_options.dart'; import 'tabs_page.dart'; @@ -62,6 +63,44 @@ class MyHomePage extends StatefulWidget { class _MyHomePageState extends State { String _message = ''; + StreamSubscription>? _purchaseSubscription; + + static const String _testProductId = '123456'; + + @override + void initState() { + super.initState(); + _purchaseSubscription = + InAppPurchase.instance.purchaseStream.listen(_onPurchaseUpdate); + } + + @override + void dispose() { + _purchaseSubscription?.cancel(); + super.dispose(); + } + + void _onPurchaseUpdate(List purchases) { + for (final purchase in purchases) { + if (purchase.pendingCompletePurchase) { + InAppPurchase.instance.completePurchase(purchase); + } + if (purchase.status == PurchaseStatus.purchased || + purchase.status == PurchaseStatus.restored) { + final transactionId = purchase.purchaseID; + print('transactionId: $transactionId'); + if (transactionId != null) { + widget.analytics.logTransaction(transactionId).then((_) { + setMessage('logTransaction succeeded with ID: $transactionId'); + }).catchError((e) { + setMessage('logTransaction failed: $e'); + }); + } + } else if (purchase.status == PurchaseStatus.error) { + setMessage('Purchase error: ${purchase.error?.message}'); + } + } + } void setMessage(String message) { setState(() { @@ -158,6 +197,40 @@ class _MyHomePageState extends State { setMessage('initiateOnDeviceConversionMeasurement succeeded'); } + Future _testLogTransaction() async { + if (kIsWeb || + (defaultTargetPlatform != TargetPlatform.iOS && + defaultTargetPlatform != TargetPlatform.macOS)) { + setMessage('logTransaction() is only supported on iOS and macOS'); + return; + } + + setMessage('Loading product $_testProductId...'); + + final response = + await InAppPurchase.instance.queryProductDetails({_testProductId}); + + if (response.error != null) { + setMessage('Failed to load product: ${response.error!.message}'); + return; + } + + if (response.productDetails.isEmpty) { + setMessage( + 'Product "$_testProductId" not found. ' + 'Make sure your StoreKit config file is set up correctly.', + ); + return; + } + + final product = response.productDetails.first; + setMessage('Initiating purchase for "${product.id}"...'); + + await InAppPurchase.instance.buyNonConsumable( + purchaseParam: PurchaseParam(productDetails: product), + ); + } + AnalyticsEventItem itemCreator() { return AnalyticsEventItem( affiliation: 'affil', @@ -365,6 +438,13 @@ class _MyHomePageState extends State { onPressed: _testInitiateOnDeviceConversionMeasurement, child: const Text('Test initiateOnDeviceConversionMeasurement'), ), + if (!kIsWeb && + (defaultTargetPlatform == TargetPlatform.iOS || + defaultTargetPlatform == TargetPlatform.macOS)) + MaterialButton( + onPressed: _testLogTransaction, + child: const Text('Test logTransaction (product: 123456)'), + ), Text( _message, style: const TextStyle(color: Color.fromARGB(255, 0, 155, 0)), diff --git a/packages/firebase_analytics/firebase_analytics/example/pubspec.yaml b/packages/firebase_analytics/firebase_analytics/example/pubspec.yaml index 02a21d62da40..d94b9a167ced 100755 --- a/packages/firebase_analytics/firebase_analytics/example/pubspec.yaml +++ b/packages/firebase_analytics/firebase_analytics/example/pubspec.yaml @@ -10,6 +10,7 @@ dependencies: firebase_core: ^4.5.0 flutter: sdk: flutter + in_app_purchase: ^3.2.3 flutter: uses-material-design: true diff --git a/packages/firebase_analytics/firebase_analytics/ios/firebase_analytics/analytics_storekit_config.storekit b/packages/firebase_analytics/firebase_analytics/ios/firebase_analytics/analytics_storekit_config.storekit new file mode 100644 index 000000000000..092f5c7b86c4 --- /dev/null +++ b/packages/firebase_analytics/firebase_analytics/ios/firebase_analytics/analytics_storekit_config.storekit @@ -0,0 +1,54 @@ +{ + "appPolicies" : { + "eula" : "", + "policies" : [ + { + "locale" : "en_US", + "policyText" : "", + "policyURL" : "" + } + ] + }, + "identifier" : "D50F15B4", + "nonRenewingSubscriptions" : [ + + ], + "products" : [ + { + "displayPrice" : "0.99", + "familyShareable" : false, + "internalID" : "FAAD0643", + "localizations" : [ + { + "description" : "", + "displayName" : "", + "locale" : "en_US" + } + ], + "productID" : "123456", + "referenceName" : "premium_upgrade", + "type" : "NonConsumable" + } + ], + "settings" : { + "_askToBuyEnabled" : false, + "_billingGracePeriodEnabled" : false, + "_billingIssuesEnabled" : false, + "_disableDialogs" : false, + "_failTransactionsEnabled" : false, + "_locale" : "en_US", + "_renewalBillingIssuesEnabled" : false, + "_storefront" : "USA", + "_storeKitErrors" : [ + + ], + "_timeRate" : 0 + }, + "subscriptionGroups" : [ + + ], + "version" : { + "major" : 4, + "minor" : 0 + } +} From 78cfd2d0de296b83d43d4e52d4156b64953867fa Mon Sep 17 00:00:00 2001 From: Jude Kwashie Date: Fri, 6 Mar 2026 10:49:32 +0000 Subject: [PATCH 12/13] chore: implement logTransaction method with unimplemented error for Android --- .../analytics/FlutterFirebaseAnalyticsPlugin.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/firebase_analytics/firebase_analytics/android/src/main/kotlin/io/flutter/plugins/firebase/analytics/FlutterFirebaseAnalyticsPlugin.kt b/packages/firebase_analytics/firebase_analytics/android/src/main/kotlin/io/flutter/plugins/firebase/analytics/FlutterFirebaseAnalyticsPlugin.kt index aca07f356d51..3c366eebf7e4 100644 --- a/packages/firebase_analytics/firebase_analytics/android/src/main/kotlin/io/flutter/plugins/firebase/analytics/FlutterFirebaseAnalyticsPlugin.kt +++ b/packages/firebase_analytics/firebase_analytics/android/src/main/kotlin/io/flutter/plugins/firebase/analytics/FlutterFirebaseAnalyticsPlugin.kt @@ -443,4 +443,16 @@ class FlutterFirebaseAnalyticsPlugin : FlutterFirebasePlugin, ) ) } + + override fun logTransaction(transactionId: String, callback: (Result) -> Unit) { + callback( + Result.failure( + FlutterError( + "unimplemented", + "logTransaction is only available on iOS.", + null + ) + ) + ) + } } From 6b8917d7a2e5c57c8b69ff82010b2a6addb9fc2f Mon Sep 17 00:00:00 2001 From: Jude Kwashie Date: Fri, 6 Mar 2026 11:03:15 +0000 Subject: [PATCH 13/13] chore: update skip conditions for Firebase Analytics E2E tests to include web platform --- .../firebase_analytics_e2e_test.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/integration_test/firebase_analytics/firebase_analytics_e2e_test.dart b/tests/integration_test/firebase_analytics/firebase_analytics_e2e_test.dart index b446d6c87193..674bfa0cdf80 100644 --- a/tests/integration_test/firebase_analytics/firebase_analytics_e2e_test.dart +++ b/tests/integration_test/firebase_analytics/firebase_analytics_e2e_test.dart @@ -373,8 +373,9 @@ void main() { ), ); }, - skip: defaultTargetPlatform != TargetPlatform.iOS && - defaultTargetPlatform != TargetPlatform.macOS, + skip: kIsWeb || + (defaultTargetPlatform != TargetPlatform.iOS && + defaultTargetPlatform != TargetPlatform.macOS), ); test( @@ -391,8 +392,9 @@ void main() { ), ); }, - skip: defaultTargetPlatform != TargetPlatform.iOS && - defaultTargetPlatform != TargetPlatform.macOS, + skip: kIsWeb || + (defaultTargetPlatform != TargetPlatform.iOS && + defaultTargetPlatform != TargetPlatform.macOS), ); }); });