Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -443,4 +443,16 @@ class FlutterFirebaseAnalyticsPlugin : FlutterFirebasePlugin,
)
)
}

override fun logTransaction(transactionId: String, callback: (Result<Unit>) -> Unit) {
callback(
Result.failure(
FlutterError(
"unimplemented",
"logTransaction is only available on iOS.",
null
)
)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ interface FirebaseAnalyticsHostApi {
fun getAppInstanceId(callback: (Result<String?>) -> Unit)
fun getSessionId(callback: (Result<Long?>) -> Unit)
fun initiateOnDeviceConversionMeasurement(arguments: Map<String, String?>, callback: (Result<Unit>) -> Unit)
fun logTransaction(transactionId: String, callback: (Result<Unit>) -> Unit)

companion object {
/** The codec used by FirebaseAnalyticsHostApi. */
Expand Down Expand Up @@ -363,6 +364,25 @@ interface FirebaseAnalyticsHostApi {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.firebase_analytics_platform_interface.FirebaseAnalyticsHostApi.logTransaction$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val transactionIdArg = args[0] as String
api.logTransaction(transactionIdArg) { result: Result<Unit> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(GeneratedAndroidFirebaseAnalyticsPigeonUtils.wrapError(error))
} else {
reply.reply(GeneratedAndroidFirebaseAnalyticsPigeonUtils.wrapResult(null))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -62,6 +63,44 @@ class MyHomePage extends StatefulWidget {

class _MyHomePageState extends State<MyHomePage> {
String _message = '';
StreamSubscription<List<PurchaseDetails>>? _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<PurchaseDetails> 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(() {
Expand Down Expand Up @@ -158,6 +197,40 @@ class _MyHomePageState extends State<MyHomePage> {
setMessage('initiateOnDeviceConversionMeasurement succeeded');
}

Future<void> _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',
Expand Down Expand Up @@ -365,6 +438,13 @@ class _MyHomePageState extends State<MyHomePage> {
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)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ dependencies:
firebase_core: ^4.5.0
flutter:
sdk: flutter
in_app_purchase: ^3.2.3

flutter:
uses-material-design: true
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ protocol FirebaseAnalyticsHostApi {
func getSessionId(completion: @escaping (Result<Int64?, Error>) -> Void)
func initiateOnDeviceConversionMeasurement(arguments: [String: String?],
completion: @escaping (Result<Void, Error>) -> Void)
func logTransaction(transactionId: String, completion: @escaping (Result<Void, Error>) -> Void)
}

/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
Expand Down Expand Up @@ -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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import firebase_core_shared
#endif
import FirebaseAnalytics
import StoreKit

let kFLTFirebaseAnalyticsName = "name"
let kFLTFirebaseAnalyticsValue = "value"
Expand All @@ -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) {
Expand Down Expand Up @@ -142,6 +145,79 @@ public class FirebaseAnalyticsPlugin: NSObject, FLTFirebasePluginProtocol, Flutt
completion(.success(()))
}

func logTransaction(transactionId: String,
completion: @escaping (Result<Void, any Error>) -> 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, any Error>) -> Void) {
Task {
do {
guard let id = UInt64(transactionId) else {
completion(.failure(FlutterError(
code: "firebase_analytics",
message: "Invalid transactionId",
details: nil
)))
return
}

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))
}
}
}

private func hexStringToData(_ hexString: String) -> Data? {
let length = hexString.count
guard length % 2 == 0 else { return nil }
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1242,6 +1242,23 @@ 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<void> logTransaction(String transactionId) async {
if (defaultTargetPlatform != TargetPlatform.iOS &&
defaultTargetPlatform != TargetPlatform.macOS) {
throw UnimplementedError(
'logTransaction() is only supported on iOS and macOS.',
);
}
return _delegate.logTransaction(transactionId: transactionId);
}

/// Sets the duration of inactivity that terminates the current session.
///
/// The default value is 1800000 milliseconds (30 minutes).
Expand Down
Loading
Loading