From 6aea970550771e8c9e896ac3e9c7012d435bb999 Mon Sep 17 00:00:00 2001 From: Paz Lavi Date: Thu, 13 Jun 2024 16:24:56 +0300 Subject: [PATCH 01/42] Purchase connector for flutter --- README.md | 1 + android/build.gradle | 25 +- .../AppsFlyerPurchaseConnector.kt | 8 + .../AppsFlyerPurchaseConnector.kt | 189 ++++++++ .../appsflyersdk/ConnectorWrapper.kt | 251 ++++++++++ .../appsflyersdk/AppsFlyerConstants.java | 1 + .../appsflyersdk/AppsflyerSdkPlugin.java | 3 + doc/PurchaseConnector.md | 322 +++++++++++++ example/android/gradle.properties | 1 + example/ios/Podfile | 4 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- ios/Classes/AppsflyerSdkPlugin.m | 5 +- .../PurchaseConnectorPlugin.swift | 152 ++++++ ios/appsflyer_sdk.podspec | 36 +- lib/appsflyer_sdk.dart | 13 + lib/appsflyer_sdk.g.dart | 431 ++++++++++++++++++ lib/src/appsflyer_constants.dart | 26 ++ .../connector_callbacks.dart | 18 + .../missing_configuration_exception.dart | 11 + .../in_app_purchase_validation_result.dart | 22 + .../purchase_connector/models/ios_error.dart | 15 + .../models/jvm_throwable.dart | 16 + .../models/product_purchase.dart | 44 ++ .../models/subscription_purchase.dart | 343 ++++++++++++++ .../subscription_validation_result.dart | 22 + .../models/validation_failure_data.dart | 20 + .../purchase_connector.dart | 268 +++++++++++ .../purchase_connector_configuration.dart | 16 + pubspec.yaml | 3 + 29 files changed, 2248 insertions(+), 20 deletions(-) create mode 100644 android/src/main/exlude-connector/com/appsflyer/appsflyersdk/AppsFlyerPurchaseConnector.kt create mode 100644 android/src/main/include-connector/com/appsflyer/appsflyersdk/AppsFlyerPurchaseConnector.kt create mode 100644 android/src/main/include-connector/com/appsflyer/appsflyersdk/ConnectorWrapper.kt create mode 100644 doc/PurchaseConnector.md create mode 100644 ios/PurchaseConnector/PurchaseConnectorPlugin.swift create mode 100644 lib/appsflyer_sdk.g.dart create mode 100644 lib/src/purchase_connector/connector_callbacks.dart create mode 100644 lib/src/purchase_connector/missing_configuration_exception.dart create mode 100644 lib/src/purchase_connector/models/in_app_purchase_validation_result.dart create mode 100644 lib/src/purchase_connector/models/ios_error.dart create mode 100644 lib/src/purchase_connector/models/jvm_throwable.dart create mode 100644 lib/src/purchase_connector/models/product_purchase.dart create mode 100644 lib/src/purchase_connector/models/subscription_purchase.dart create mode 100644 lib/src/purchase_connector/models/subscription_validation_result.dart create mode 100644 lib/src/purchase_connector/models/validation_failure_data.dart create mode 100644 lib/src/purchase_connector/purchase_connector.dart create mode 100644 lib/src/purchase_connector/purchase_connector_configuration.dart diff --git a/README.md b/README.md index 130c50f4..36a0a8a1 100644 --- a/README.md +++ b/README.md @@ -64,4 +64,5 @@ You can read more about it [here](https://dev.appsflyer.com/hc/docs/install-andr - [Advanced APIs](/doc/AdvancedAPI.md) - [Testing the integration](/doc/Testing.md) - [APIs](/doc/API.md) +- [Purchase Connector](/doc/PurchaseConnector.md) - [Sample App](/example) diff --git a/android/build.gradle b/android/build.gradle index dfae0b92..d6077efe 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -14,10 +14,14 @@ rootProject.allprojects { } } apply plugin: 'com.android.library' +apply plugin: 'org.jetbrains.kotlin.android' + +def includeConnector = project.findProperty('appsflyer.enable_purchase_connector')?.toBoolean() ?: false + android { defaultConfig { - minSdkVersion 16 - compileSdk 31 + minSdkVersion 19 + compileSdk 33 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" multiDexEnabled true @@ -26,11 +30,26 @@ android { disable 'InvalidPackage' } namespace 'com.appsflyer.appsflyersdk' + + sourceSets { + main { + java.srcDirs = ['src/main/java'] + java.srcDirs += includeConnector ? ['src/main/include-connector'] : ['src/main/exlude-connector'] + } + includeConnector ? ['src/main/include-connector'] : ['src/main/exlude-connector'] + } + kotlinOptions { + jvmTarget = '17' + } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.appcompat:appcompat:1.0.0' implementation 'com.appsflyer:af-android-sdk:6.14.0' - implementation 'com.android.installreferrer:installreferrer:2.1' + implementation 'com.android.installreferrer:installreferrer:2.2' +// implementation 'androidx.core:core-ktx:1.13.1' + if (includeConnector){ + implementation 'com.appsflyer:purchase-connector:2.0.1' + } } \ No newline at end of file diff --git a/android/src/main/exlude-connector/com/appsflyer/appsflyersdk/AppsFlyerPurchaseConnector.kt b/android/src/main/exlude-connector/com/appsflyer/appsflyersdk/AppsFlyerPurchaseConnector.kt new file mode 100644 index 00000000..188033f0 --- /dev/null +++ b/android/src/main/exlude-connector/com/appsflyer/appsflyersdk/AppsFlyerPurchaseConnector.kt @@ -0,0 +1,8 @@ +package com.appsflyer.appsflyersdk + +import io.flutter.embedding.engine.plugins.FlutterPlugin + +object AppsFlyerPurchaseConnector: FlutterPlugin { + override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) = Unit + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) = Unit +} \ No newline at end of file diff --git a/android/src/main/include-connector/com/appsflyer/appsflyersdk/AppsFlyerPurchaseConnector.kt b/android/src/main/include-connector/com/appsflyer/appsflyersdk/AppsFlyerPurchaseConnector.kt new file mode 100644 index 00000000..0b57a576 --- /dev/null +++ b/android/src/main/include-connector/com/appsflyer/appsflyersdk/AppsFlyerPurchaseConnector.kt @@ -0,0 +1,189 @@ +package com.appsflyer.appsflyersdk + +import android.content.Context +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import java.lang.ref.WeakReference + +/** + * A Flutter plugin that establishes a bridge between the Flutter appsflyer SDK and the Native Android Purchase Connector. + * + * This plugin utilizes MethodChannels to communicate between Flutter and native Android, + * passing method calls and event callbacks. + * + * @property methodChannel used to set up the communication channel between Flutter and Android. + * @property contextRef a Weak Reference to the application context when the plugin is first attached. Used to build the Appsflyer's Purchase Connector. + * @property connectorWrapper wraps the Appsflyer's Android purchase client and bridge map conversion methods. Used to perform various operations (configure, start/stop observing transactions). + * @property arsListener an object of [MappedValidationResultListener] that handles SubscriptionPurchaseValidationResultListener responses and failures. Lazily initialized. + * @property viapListener an object of [MappedValidationResultListener] that handles InAppValidationResultListener responses and failures. Lazily initialized. + */ +object AppsFlyerPurchaseConnector : FlutterPlugin, MethodChannel.MethodCallHandler { + private var methodChannel: MethodChannel? = null + private var contextRef: WeakReference? = null + private var connectorWrapper: ConnectorWrapper? = null + + private val arsListener: MappedValidationResultListener by lazy { + object : MappedValidationResultListener { + override fun onFailure(result: String, error: Throwable?) { + val resMap = mapOf("result" to result, "error" to error?.toMap()) + methodChannel?.invokeMethod( + "SubscriptionPurchaseValidationResultListener:onFailure", + resMap + ) + } + + override fun onResponse(p0: Map?) { + methodChannel?.invokeMethod( + "SubscriptionPurchaseValidationResultListener:onResponse", + p0 + ) + } + } + } + + private val viapListener: MappedValidationResultListener by lazy { + object : MappedValidationResultListener { + override fun onFailure(result: String, error: Throwable?) { + val resMap = mapOf("result" to result, "error" to error?.toMap()) + methodChannel?.invokeMethod("InAppValidationResultListener:onFailure", resMap) + } + + override fun onResponse(p0: Map?) { + methodChannel?.invokeMethod("InAppValidationResultListener:onResponse", p0) + } + } + } + + /** + * Called when the plugin is attached to the Flutter engine. + * + * It sets up the MethodChannel and retains the application context. + * + * @param binding The binding provides access to the binary messenger and application context. + */ + override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { + methodChannel = + MethodChannel( + binding.binaryMessenger, + AppsFlyerConstants.AF_PURCHASE_CONNECTOR_CHANNEL + ).also { + it.setMethodCallHandler(this) + } + contextRef = WeakReference(binding.applicationContext) + } + + /** + * Called when the plugin is detached from the Flutter engine. + * + * @param binding The binding that was provided in [onAttachedToEngine]. + */ + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) = Unit + + /** + * Handles incoming method calls from Flutter. + * + * It either triggers a connector operation or returns an unimplemented error. + * Supported operations are configuring, starting and stopping observing transactions. + * + * @param call The method call from Flutter. + * @param result The result to be returned to Flutter. + */ + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + when (call.method) { + "startObservingTransactions" -> startObservingTransactions(result) + "stopObservingTransactions" -> stopObservingTransactions(result) + "configure" -> configure(call, result) + else -> result.notImplemented() + } + } + + /** + * Configures the purchase connector with the parameters sent from Flutter. + * + * @param call The method call from Flutter. + * @param result The result to be returned to Flutter. + */ + private fun configure(call: MethodCall, result: MethodChannel.Result) { + if (connectorWrapper == null) { + contextRef?.get()?.let { ctx -> + connectorWrapper = ConnectorWrapper( + ctx, call.getBoolean("logSubscriptions"), + call.getBoolean("logInApps"), + call.getBoolean("sandbox"), + arsListener, viapListener + ) + result.success(null) + } ?: run { + result.error("402", "Missing context. Is plugin attached to engine?", null) + } + + } else { + result.error("401", "Connector already configured", null) + } + } + + /** + * Starts observing transactions. + * + * @param result The result to be returned to Flutter. + */ + private fun startObservingTransactions(result: MethodChannel.Result) = + connectorOperation(result) { + it.startObservingTransactions() + } + + /** + * Stops observing transactions. + * + * @param result The result to be returned to Flutter. + */ + private fun stopObservingTransactions(result: MethodChannel.Result) = + connectorOperation(result) { + it.stopObservingTransactions() + } + + /** + * Performs a specified operation on the connector after confirming that the connector has been configured. + * + * @param result The result to be returned to Flutter. + * @param exc The operation to be performed on the connector. + */ + private fun connectorOperation( + result: MethodChannel.Result, + exc: (connectorWrapper: ConnectorWrapper) -> Unit + ) { + if (connectorWrapper != null) { + exc(connectorWrapper!!) + result.success(null) + } else { + result.error("404", "Connector not configured, did you called `configure` first?", null) + } + } + /** + * Converts a [Throwable] to a Map that can be returned to Flutter. + * + * @return A map representing the [Throwable]. + */ + private fun Throwable.toMap(): Map { + return mapOf( + "type" to this::class.simpleName, + "message" to this.message, + "stacktrace" to this.stackTrace.joinToString(separator = "\n") { it.toString() }, + "cause" to this.cause?.toMap() + ) + } + + /** + * Attempts to get a Boolean argument from the method call. + * + * If unsuccessful, it returns the default value. + * + * @param key The key for the argument. + * @param defValue The default value to be returned if the argument does not exist. + * @return The value of the argument or the default value if the argument does not exist. + */ + private fun MethodCall.getBoolean(key: String, defValue: Boolean = false): Boolean = + runCatching { argument(key)!! }.getOrDefault(defValue) + +} \ No newline at end of file diff --git a/android/src/main/include-connector/com/appsflyer/appsflyersdk/ConnectorWrapper.kt b/android/src/main/include-connector/com/appsflyer/appsflyersdk/ConnectorWrapper.kt new file mode 100644 index 00000000..cc9e2bfc --- /dev/null +++ b/android/src/main/include-connector/com/appsflyer/appsflyersdk/ConnectorWrapper.kt @@ -0,0 +1,251 @@ +package com.appsflyer.appsflyersdk + +import android.content.Context +import com.appsflyer.api.PurchaseClient +import com.appsflyer.api.Store +import com.appsflyer.internal.models.* +import com.appsflyer.internal.models.InAppPurchaseValidationResult +import com.appsflyer.internal.models.SubscriptionPurchase +import com.appsflyer.internal.models.SubscriptionValidationResult +import com.appsflyer.internal.models.ValidationFailureData + +interface MappedValidationResultListener : PurchaseClient.ValidationResultListener> + +/** + * A connector class that wraps the Android purchase connector client. + * + * This class uses the Builder pattern to configure the Android purchase connector client. + * It implements the [PurchaseClient] interface required by the appsflyer_sdk and translates + * the various callbacks and responses between the two interfaces. + * + * @property context The application context. + * @property logSubs If true, subscription transactions will be logged. + * @property logInApps If true, in-app purchase transactions will be logged. + * @property sandbox If true, the purchase client will be in sandbox mode. + * @property subsListener The listener for subscription purchase validation results. + * @property inAppListener The listener for in-app purchase validation Result. + */ +class ConnectorWrapper( + context: Context, + logSubs: Boolean, + logInApps: Boolean, + sandbox: Boolean, + subsListener: MappedValidationResultListener, + inAppListener: MappedValidationResultListener, +) : + PurchaseClient { + private val connector = + PurchaseClient.Builder(context, Store.GOOGLE).setSandbox(sandbox).logSubscriptions(logSubs) + .autoLogInApps(logInApps).setSubscriptionValidationResultListener(object : + PurchaseClient.SubscriptionPurchaseValidationResultListener { + override fun onResponse(result: Map?) { + subsListener.onResponse(result?.entries?.associate { (k, v) -> k to v.toJsonMap() }) + } + + override fun onFailure(result: String, error: Throwable?) { + subsListener.onFailure(result, error) + } + }).setInAppValidationResultListener(object : PurchaseClient.InAppPurchaseValidationResultListener{ + override fun onResponse(result: Map?) { + inAppListener.onResponse(result?.entries?.associate { (k, v) -> k to v.toJsonMap() }) + } + + override fun onFailure(result: String, error: Throwable?) { + inAppListener.onFailure(result, error) + } + }) + .build() + + /** + * Starts observing all incoming transactions from the play store. + */ + override fun startObservingTransactions() = connector.startObservingTransactions() + + /** + * Stops observing all incoming transactions from the play store. + */ + override fun stopObservingTransactions() = connector.stopObservingTransactions() + + + /** + * Converts [SubscriptionPurchase] to a Json map, which then is delivered to SDK's method response. + * + * @return A map representing this SubscriptionPurchase. + */ + private fun SubscriptionPurchase.toJsonMap(): Map { + return mapOf( + "acknowledgementState" to acknowledgementState, + "canceledStateContext" to canceledStateContext?.toJsonMap(), + "externalAccountIdentifiers" to externalAccountIdentifiers?.toJsonMap(), + "kind" to kind, + "latestOrderId" to latestOrderId, + "lineItems" to lineItems.map { it.toJsonMap() }, + "linkedPurchaseToken" to linkedPurchaseToken, + "pausedStateContext" to pausedStateContext?.toJsonMap(), + "regionCode" to regionCode, + "startTime" to startTime, + "subscribeWithGoogleInfo" to subscribeWithGoogleInfo?.toJsonMap(), + "subscriptionState" to subscriptionState, + "testPurchase" to testPurchase?.toJsonMap() + ) + } + + private fun CanceledStateContext.toJsonMap(): Map { + return mapOf( + "developerInitiatedCancellation" to developerInitiatedCancellation?.toJsonMap(), + "replacementCancellation" to replacementCancellation?.toJsonMap(), + "systemInitiatedCancellation" to systemInitiatedCancellation?.toJsonMap(), + "userInitiatedCancellation" to userInitiatedCancellation?.toJsonMap() + ) + } + + private fun DeveloperInitiatedCancellation.toJsonMap(): Map { + return mapOf() + } + + private fun ReplacementCancellation.toJsonMap(): Map { + return mapOf() + } + + private fun SystemInitiatedCancellation.toJsonMap(): Map { + return mapOf() + } + + private fun UserInitiatedCancellation.toJsonMap(): Map { + return mapOf( + "cancelSurveyResult" to cancelSurveyResult?.toJsonMap(), + "cancelTime" to cancelTime + ) + } + + private fun CancelSurveyResult.toJsonMap(): Map { + return mapOf( + "reason" to reason, + "reasonUserInput" to reasonUserInput + ) + } + + private fun ExternalAccountIdentifiers.toJsonMap(): Map { + return mapOf( + "externalAccountId" to externalAccountId, + "obfuscatedExternalAccountId" to obfuscatedExternalAccountId, + "obfuscatedExternalProfileId" to obfuscatedExternalProfileId + ) + } + + private fun SubscriptionPurchaseLineItem.toJsonMap(): Map { + return mapOf( + "autoRenewingPlan" to autoRenewingPlan?.toJsonMap(), + "deferredItemReplacement" to deferredItemReplacement?.toJsonMap(), + "expiryTime" to expiryTime, + "offerDetails" to offerDetails?.toJsonMap(), + "prepaidPlan" to prepaidPlan?.toJsonMap(), + "productId" to productId + ) + } + + private fun OfferDetails.toJsonMap(): Map { + return mapOf( + "offerTags" to offerTags, + "basePlanId" to basePlanId, + "offerId" to offerId + ) + } + + private fun AutoRenewingPlan.toJsonMap(): Map { + return mapOf( + "autoRenewEnabled" to autoRenewEnabled, + "priceChangeDetails" to priceChangeDetails?.toJsonMap() + ) + } + + private fun SubscriptionItemPriceChangeDetails.toJsonMap(): Map { + return mapOf( + "expectedNewPriceChargeTime" to expectedNewPriceChargeTime, + "newPrice" to newPrice?.toJsonMap(), + "priceChangeMode" to priceChangeMode, + "priceChangeState" to priceChangeState + ) + } + + private fun Money.toJsonMap(): Map { + return mapOf( + "currencyCode" to currencyCode, + "nanos" to nanos, + "units" to units + ) + } + + private fun DeferredItemReplacement.toJsonMap(): Map { + return mapOf("productId" to productId) + } + + private fun PrepaidPlan.toJsonMap(): Map { + return mapOf("allowExtendAfterTime" to allowExtendAfterTime) + } + + private fun PausedStateContext.toJsonMap(): Map { + return mapOf("autoResumeTime" to autoResumeTime) + } + + private fun SubscribeWithGoogleInfo.toJsonMap(): Map { + return mapOf( + "emailAddress" to emailAddress, + "familyName" to familyName, + "givenName" to givenName, + "profileId" to profileId, + "profileName" to profileName + ) + } + + fun TestPurchase.toJsonMap(): Map { + return mapOf() + } + + private fun ProductPurchase.toJsonMap(): Map { + return mapOf( + "kind" to kind, + "purchaseTimeMillis" to purchaseTimeMillis, + "purchaseState" to purchaseState, + "consumptionState" to consumptionState, + "developerPayload" to developerPayload, + "orderId" to orderId, + "purchaseType" to purchaseType, + "acknowledgementState" to acknowledgementState, + "purchaseToken" to purchaseToken, + "productId" to productId, + "quantity" to quantity, + "obfuscatedExternalAccountId" to obfuscatedExternalAccountId, + "obfuscatedExternalProfileId" to obfuscatedExternalProfileId, + "regionCode" to regionCode + ) + } + + /** + * Converts [InAppPurchaseValidationResult] into a map of objects so that the Object can be passed to Flutter using a method channel + * + * @return A map representing this InAppPurchaseValidationResult. + */ + private fun InAppPurchaseValidationResult.toJsonMap(): Map { + return mapOf( + "success" to success, + "productPurchase" to productPurchase?.toJsonMap(), + "failureData" to failureData?.toJsonMap() + ) + } + + private fun SubscriptionValidationResult.toJsonMap(): Map { + return mapOf( + "success" to success, + "subscriptionPurchase" to subscriptionPurchase?.toJsonMap(), + "failureData" to failureData?.toJsonMap() + ) + } + + private fun ValidationFailureData.toJsonMap(): Map { + return mapOf( + "status" to status, + "description" to description + ) + } +} \ No newline at end of file diff --git a/android/src/main/java/com/appsflyer/appsflyersdk/AppsFlyerConstants.java b/android/src/main/java/com/appsflyer/appsflyersdk/AppsFlyerConstants.java index 36a8c259..df18069d 100644 --- a/android/src/main/java/com/appsflyer/appsflyersdk/AppsFlyerConstants.java +++ b/android/src/main/java/com/appsflyer/appsflyersdk/AppsFlyerConstants.java @@ -24,6 +24,7 @@ public class AppsFlyerConstants { final static String AF_EVENTS_CHANNEL = "af-events"; final static String AF_METHOD_CHANNEL = "af-api"; + final static String AF_PURCHASE_CONNECTOR_CHANNEL = "af-purchase-connector"; final static String AF_CALLBACK_CHANNEL = "callbacks"; final static String AF_BROADCAST_ACTION_NAME = "com.appsflyer.appsflyersdk"; diff --git a/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java b/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java index 4c450256..60d4340f 100644 --- a/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java +++ b/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java @@ -1008,6 +1008,7 @@ private Map replaceNullValues(Map map) { @Override public void onAttachedToEngine(FlutterPluginBinding binding) { onAttachedToEngine(binding.getApplicationContext(), binding.getBinaryMessenger()); + AppsFlyerPurchaseConnector.INSTANCE.onAttachedToEngine(binding); } @Override @@ -1016,6 +1017,8 @@ public void onDetachedFromEngine(FlutterPluginBinding binding) { mMethodChannel = null; mEventChannel.setStreamHandler(null); mEventChannel = null; + AppsFlyerPurchaseConnector.INSTANCE.onDetachedFromEngine(binding); + } @Override diff --git a/doc/PurchaseConnector.md b/doc/PurchaseConnector.md new file mode 100644 index 00000000..db266895 --- /dev/null +++ b/doc/PurchaseConnector.md @@ -0,0 +1,322 @@ + +# Flutter Purchase Connector +**At a glance:** Automatically validate and measure revenue from in-app purchases and auto-renewable subscriptions to get the full picture of your customers' life cycles and accurate ROAS measurements. +For more information please check the following pages: +* [ROI360 in-app purchase (IAP) and subscription revenue measurement](https://support.appsflyer.com/hc/en-us/articles/7459048170769-ROI360-in-app-purchase-IAP-and-subscription-revenue-measurement?query=purchase) +* [Android Purchase Connector](https://dev.appsflyer.com/hc/docs/purchase-connector-android) +* [iOS Purchase Connector](https://dev.appsflyer.com/hc/docs/purchase-connector-ios) + +🛠 In order for us to provide optimal support, we would kindly ask you to submit any issues to +support@appsflyer.com + +> *When submitting an issue please specify your AppsFlyer sign-up (account) email , your app ID , production steps, logs, code snippets and any additional relevant information.* + +## Table Of Content + +* [ âš ī¸ âš ī¸ Important Note âš ī¸ âš ī¸ ](#-important-note-) +* [ Adding The Connector To Your Project](#adding-the-connector-to-your-project) + + [How to Opt-In](#how-to-opt-in) + + [What Happens if You Use Dart Files Without Opting In?](#what-happens-if-you-use-dart-files-without-opting-in) +* [ Basic Integration Of The Connector](#basic-integration-of-the-connector) + + [ Create PurchaseConnector Instance](#create-purchaseconnector-instance) + + [ Start Observing Transactions](#start-observing-transactions) + + [ Stop Observing Transactions](#stop-observing-transactions) + + [ Log Subscriptions](#log-subscriptions) + + [ Log In App Purchases](#log-in-app-purchases) +* [ Register Validation Results Listeners](#register-validation-results-listeners) + + [ Cross-Platform Considerations](#cross-platform-considerations) + + [ Android Callback Types](#android-callback-types) + + [ Android - Subscription Validation Result Listener ](#android-subscription-validation-result-listener) + + [ Android In Apps Validation Result Listener](#android-in-apps-validation-result-listener) + + [ iOS Combined Validation Result Listener](#ios-combined-validation-result-listener) +* [ Testing the Integration](#testing-the-integration) + + [ Android](#android) + + [ iOS](#ios) + + [ Dart Usage for Android and iOS](#dart-usage-for-android-and-ios) +* [ ProGuard Rules for Android](#proguard-rules-for-android) +* [ Full Code Example](#full-code-example) + + + +## âš ī¸ âš ī¸ Important Note âš ī¸ âš ī¸ + +The Purchase Connector feature of the AppsFlyer SDK depends on specific libraries provided by Google and Apple for managing in-app purchases: + +- For Android, it depends on the [Google Play Billing Library](https://developer.android.com/google/play/billing/integrate) (Supported versions: 5.x.x - 6.x.x). +- For iOS, it depends on [StoreKit](https://developer.apple.com/documentation/storekit). + +However, these dependencies aren't actively included with the SDK. This means that the responsibility of managing these dependencies and including the necessary libraries in your project falls on you as the consumer of the SDK. + +If you're implementing in-app purchases in your app, you'll need to ensure that the Google Play Billing Library (for Android) or StoreKit (for iOS) are included in your project. You can include these libraries manually in your native code, or you can use a third-party Flutter plugin, such as the [`in_app_purchase`](https://pub.dev/packages/in_app_purchase) plugin. + +Remember to appropriately manage these dependencies when implementing the Purchase Validation feature in your app. Failing to include the necessary libraries might result in failures when attempting to conduct in-app purchases or validate purchases. + +## Adding The Connector To Your Project + +The Purchase Connector feature in AppsFlyer SDK Flutter Plugin is an optional enhancement that you can choose to use based on your requirements. This feature is not included by default and you'll have to opt-in if you wish to use it. + +### How to Opt-In + +To opt-in and include this feature in your app, you need to set specific properties based on your platform: + +For **iOS**, in your Podfile located within the `iOS` folder of your Flutter project, set `$AppsFlyerPurchaseConnector` to `true`. +```ruby +$AppsFlyerPurchaseConnector = true +``` +For **Android**, in your `gradle.properties` file located within the `Android` folder of your Flutter project,, set `appsflyer.enable_purchase_connector` to `true`. +```groovy +appsflyer.enable_purchase_connector=true +``` +Once you set these properties, the Purchase Validation feature will be integrated into your project and you can utilize its functionality in your app. + +### What Happens if You Use Dart Files Without Opting In? + +The Dart files for the Purchase Validation feature are always included in the plugin. If you try to use these Dart APIs without opting into the feature, the APIs will not have effect because the corresponding native code necessary for them to function will not be included in your project. + +In such cases, you'll likely experience errors or exceptions when trying to use functionalities provided by the Purchase Validation feature. To avoid these issues, ensure that you opt-in to the feature if you intend to use any related APIs. + +## Basic Integration Of The Connector +### Create PurchaseConnector Instance +The `PurchaseConnector` requires a configuration object of type `PurchaseConnectorConfiguration` at instantiation time. This configuration object governs how the `PurchaseConnector` behaves in your application. + +To properly set up the configuration object, you must specify certain parameters: + +- `logSubscriptions`: If set to `true`, the connector logs all subscription events. +- `logInApps`: If set to `true`, the connector logs all in-app purchase events. +- `sandbox`: If set to `true`, transactions are tested in a sandbox environment. Be sure to set this to `false` in production. + +Here's an example usage: + +```dart +void main() { + final afPurchaseClient = PurchaseConnector( + config: PurchaseConnectorConfiguration( + logSubscriptions: true, // Enables logging of subscription events + logInApps: true, // Enables logging of in-app purchase events + sandbox: true, // Enables testing in a sandbox environment + ), + ); + + // Continue with your application logic... +} +``` + +**IMPORTANT**: The `PurchaseConnectorConfiguration` is required only the first time you instantiate `PurchaseConnector`. If you attempt to create a `PurchaseConnector` instance and no instance has been initialized yet, you must provide a `PurchaseConnectorConfiguration`. If an instance already exists, the system will ignore the configuration provided and will return the existing instance to enforce the singleton pattern. + +For example: + +```dart +void main() { + // Correct usage: Providing configuration at first instantiation + final purchaseConnector1 = PurchaseConnector( + config: PurchaseConnectorConfiguration( + logSubscriptions: true, + logInApps: true, + sandbox: true, + ), + ); + + // Additional instantiations will ignore the provided configuration + // and will return the previously created instance. + final purchaseConnector2 = PurchaseConnector( + config: PurchaseConnectorConfiguration( + logSubscriptions: false, + logInApps: false, + sandbox: false, + ), + ); + + // purchaseConnector1 and purchaseConnector2 point to the same instance + assert(purchaseConnector1 == purchaseConnector2); +} +``` + +Thus, always ensure that the initial configuration fully suits your requirements, as subsequent changes are not considered. + +Remember to set `sandbox` to `false` before releasing your app to production. If the production purchase event is sent in sandbox mode, your event won't be validated properly by AppsFlyer. +### Start Observing Transactions +Start the SDK instance to observe transactions.
+ +**âš ī¸ Please Note** +> This should be called right after calling the `AppsflyerSdk` [start](https://github.com/AppsFlyerSDK/appsflyer-flutter-plugin/blob/master/doc/BasicIntegration.md#startsdk). +> Calling `startObservingTransactions` activates a listener that automatically observes new billing transactions. This includes new and existing subscriptions and new in app purchases. +> The best practice is to activate the listener as early as possible. +```dart + // start + afPurchaseClient.startObservingTransactions(); +``` + +###
Stop Observing Transactions +Stop the SDK instance from observing transactions.
+**âš ī¸ Please Note** +> This should be called if you would like to stop the Connector from listening to billing transactions. This removes the listener and stops observing new transactions. +> An example for using this API is if the app wishes to stop sending data to AppsFlyer due to changes in the user's consent (opt-out from data sharing). Otherwise, there is no reason to call this method. +> If you do decide to use it, it should be called right before calling the Android SDK's [`stop`](https://dev.appsflyer.com/hc/docs/android-sdk-reference-appsflyerlib#stop) API + +```dart + // start + afPurchaseClient.stopObservingTransactions(); +``` + +###
Log Subscriptions +Enables automatic logging of subscription events.
+Set true to enable, false to disable.
+If this field is not used, by default, the connector will not record Subscriptions.
+```dart +final afPurchaseClient = PurchaseConnector( + config: PurchaseConnectorConfiguration(logSubscriptions: true)); +``` + +###
Log In App Purchases +Enables automatic logging of In-App purchase events
+Set true to enable, false to disable.
+If this field is not used, by default, the connector will not record In App Purchases.
+ +```dart +final afPurchaseClient = PurchaseConnector( + config: PurchaseConnectorConfiguration(logInApps: true)); +``` + + +##
Register Validation Results Listeners +You can register listeners to get the validation results once getting a response from AppsFlyer servers to let you know if the purchase was validated successfully.
+ +###
Cross-Platform Considerations + +The AppsFlyer SDK Flutter plugin acts as a bridge between your Flutter app and the underlying native SDKs provided by AppsFlyer. It's crucial to understand that the native infrastructure of iOS and Android is quite different, and so is the AppsFlyer SDK built on top of them. These differences are reflected in how you would handle callbacks separately for each platform. + +In the iOS environment, there is a single callback method `didReceivePurchaseRevenueValidationInfo` to handle both subscriptions and in-app purchases. You set this callback using `setDidReceivePurchaseRevenueValidationInfo`. + +On the other hand, Android segregates callbacks for subscriptions and in-app purchases. It provides two separate listener methods - `setSubscriptionValidationResultListener` for subscriptions and `setInAppValidationResultListener` for in-app purchases. These listener methods register callback handlers for `OnResponse` (executed when a successful response is received) and `OnFailure` (executed when a failure occurs, including due to a network exception or non-200/OK response from the server). + +By splitting the callbacks, you can ensure platform-specific responses and tailor your app's behavior accordingly. It's crucial to consider these nuances to ensure a smooth integration of AppsFlyer SDK into your Flutter application. + +### Android Callback Types + +| Listener Method | Description | +|-------------------------------|--------------| +| `onResponse(result: Result?)` | Invoked when we got 200 OK response from the server (INVALID purchase is considered to be successful response and will be returned to this callback) | +|`onFailure(result: String, error: Throwable?)`|Invoked when we got some network exception or non 200/OK response from the server.| + +### Android - Subscription Validation Result Listener + +```dart +// set listeners for Android +afPurchaseClient.setSubscriptionValidationResultListener( + (Map? result) { + // handle subscription validation result for Android +}, (String result, JVMThrowable? error) { + // handle subscription validation error for Android +}); +``` + +### Android In Apps Validation Result Listener +```dart +afPurchaseClient.setInAppValidationResultListener( + (Map? result) { + // handle in-app validation result for Android + }, (String result, JVMThrowable? error) { + // handle in-app validation error for Android +}); +``` + +### iOS Combined Validation Result Listener +```dart +afPurchaseClient.setDidReceivePurchaseRevenueValidationInfo((validationInfo, error) { + // handle subscription and in-app validation result and errors for iOS +}); +``` + + +## Testing the Integration + +With the AppsFlyer SDK, you can select which environment will be used for validation - either **production** or **sandbox**. By default, the environment is set to production. However, while testing your app, you should use the sandbox environment. + +### Android + +For Android, testing your integration with the [Google Play Billing Library](https://developer.android.com/google/play/billing/test) should use the sandbox environment. + +To set the environment to sandbox in Flutter, just set the `sandbox` parameter in the `PurchaseConnectorConfiguration` to `true` when instantiating `PurchaseConnector`. + +Remember to switch the environment back to production (set `sandbox` to `false`) before uploading your app to the Google Play Store. + +### iOS + +To test purchases in an iOS environment on a real device with a TestFlight sandbox account, you also need to set `sandbox` to `true`. + +> *IMPORTANT NOTE: Before releasing your app to production please be sure to set `sandbox` to `false`. If a production purchase event is sent in sandbox mode, your event will not be validated properly! * + +### Dart Usage for Android and iOS + +For both Android and iOS, you can set the sandbox environment using the `sandbox` parameter in the `PurchaseConnectorConfiguration` when you instantiate `PurchaseConnector` in your Dart code like this: + +```dart +// Testing in a sandbox environment +final purchaseConnector = PurchaseConnector( + PurchaseConnectorConfiguration(sandbox: true) +); +``` + +Remember to set `sandbox` back to `false` before releasing your app to production. If the production purchase event is sent in sandbox mode, your event won't be validated properly. + +## ProGuard Rules for Android + +If you are using ProGuard to obfuscate your APK for Android, you need to ensure that it doesn't interfere with the functionality of AppsFlyer SDK and its Purchase Connector feature. + +Add following keep rules to your `proguard-rules.pro` file: + +```groovy +-keep class com.appsflyer.** { *; } +-keep class kotlin.jvm.internal.Intrinsics{ *; } +-keep class kotlin.collections.**{ *; } +``` + +## Full Code Example +```dart +PurchaseConnectorConfiguration config = PurchaseConnectorConfiguration( + logSubscriptions: true, logInApps: true, sandbox: false); +final afPurchaseClient = PurchaseConnector(config: config); + +// set listeners for Android +afPurchaseClient.setSubscriptionValidationResultListener( + (Map? result) { + // handle subscription validation result for Android + result?.entries.forEach((element) { + debugPrint( + "Subscription Validation Result\n\t Token: ${element.key}\n\tresult: ${jsonEncode(element.value.toJson())}"); + }); +}, (String result, JVMThrowable? error) { + // handle subscription validation error for Android + var errMsg = error != null ? jsonEncode(error.toJson()) : null; + debugPrint( + "Subscription Validation Result\n\t result: $result\n\terror: $errMsg"); +}); + +afPurchaseClient.setInAppValidationResultListener( + (Map? result) { + // handle in-app validation result for Android + result?.entries.forEach((element) { + debugPrint( + "In App Validation Result\n\t Token: ${element.key}\n\tresult: ${jsonEncode(element.value.toJson())}"); + }); +}, (String result, JVMThrowable? error) { + // handle in-app validation error for Android + var errMsg = error != null ? jsonEncode(error.toJson()) : null; + debugPrint( + "In App Validation Result\n\t result: $result\n\terror: $errMsg"); +}); + +// set listener for iOS +afPurchaseClient + .setDidReceivePurchaseRevenueValidationInfo((validationInfo, error) { + var validationInfoMsg = + validationInfo != null ? jsonEncode(validationInfo) : null; + var errMsg = error != null ? jsonEncode(error.toJson()) : null; + debugPrint( + "iOS Validation Result\n\t validationInfo: $validationInfoMsg\n\terror: $errMsg"); + // handle subscription and in-app validation result and errors for iOS +}); + +// start +afPurchaseClient.startObservingTransactions(); +``` \ No newline at end of file diff --git a/example/android/gradle.properties b/example/android/gradle.properties index 94adc3a3..2e2c8994 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -1,3 +1,4 @@ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true +appsflyer.enable_purchase_connector=true \ No newline at end of file diff --git a/example/ios/Podfile b/example/ios/Podfile index a72f9d72..280bf5f3 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -3,7 +3,7 @@ # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' - +$AppsFlyerPurchaseConnector = true project 'Runner', { 'Debug' => :debug, 'Profile' => :release, @@ -30,7 +30,7 @@ flutter_ios_podfile_setup target 'Runner' do use_frameworks! use_modular_headers! - + # Include appsflyer_sdk/PurchaseConnector as a pod dependency flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) target 'RunnerTests' do inherit! :search_paths diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 87131a09..8e3ca5df 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ +#import "appsflyer_sdk/appsflyer_sdk-Swift.h" typedef void (*bypassDidFinishLaunchingWithOption)(id, SEL, NSInteger); @@ -56,6 +57,9 @@ - (instancetype)initWithMessenger:(nonnull NSObject *)me } + (void)registerWithRegistrar:(NSObject*)registrar { +#ifdef ENABLE_PURCHASE_CONNECTOR + [PurchaseConnectorPlugin registerWithRegistrar:registrar]; +#endif id messenger = [registrar messenger]; FlutterMethodChannel *channel = [FlutterMethodChannel methodChannelWithName:afMethodChannel binaryMessenger:messenger]; FlutterMethodChannel *callbackChannel = [FlutterMethodChannel methodChannelWithName:afCallbacksMethodChannel binaryMessenger:messenger]; @@ -193,7 +197,6 @@ - (void)setConsentData:(FlutterMethodCall*)call result:(FlutterResult)result { BOOL isUserSubjectToGDPR = [consentDict[@"isUserSubjectToGDPR"] boolValue]; BOOL hasConsentForDataUsage = [consentDict[@"hasConsentForDataUsage"] boolValue]; BOOL hasConsentForAdsPersonalization = [consentDict[@"hasConsentForAdsPersonalization"] boolValue]; - AppsFlyerConsent *consentData; if(isUserSubjectToGDPR){ consentData = [[AppsFlyerConsent alloc] initForGDPRUserWithHasConsentForDataUsage:hasConsentForDataUsage diff --git a/ios/PurchaseConnector/PurchaseConnectorPlugin.swift b/ios/PurchaseConnector/PurchaseConnectorPlugin.swift new file mode 100644 index 00000000..559f683e --- /dev/null +++ b/ios/PurchaseConnector/PurchaseConnectorPlugin.swift @@ -0,0 +1,152 @@ +// +// PurchaseConnectorPlugin.swift +// appsflyer_sdk +// +// Created by Paz Lavi on 11/06/2024. +// +import Foundation +import PurchaseConnector +import Flutter + +/// `PurchaseConnectorPlugin` is a `FlutterPlugin` implementation that acts as the bridge between Flutter and the PurchaseConnector iOS SDK. +/// This class is responsible for processing incoming method calls from the Dart layer (via a MethodChannel) and translating these calls to the appropriate tasks in the PurchaseConnector SDK. +@objc public class PurchaseConnectorPlugin : NSObject, FlutterPlugin { + + /// Methods channel name constant to be used across plugin. + private static let AF_PURCHASE_CONNECTOR_CHANNEL = "af-purchase-connector" + + /// Singleton instance of `PurchaseConnectorPlugin` ensures this plugin acts as a centralized point of contact for all method calls. + internal static let shared = PurchaseConnectorPlugin() + + /// An instance of `PurchaseConnector`. + /// This will be intentionally set to `nil` by default and will be initialized once we call the `configure` method via Flutter. + private var connector: PurchaseConnector? = nil + + /// Instance of method channel providing a bridge to Dart code. + private var methodChannel: FlutterMethodChannel? = nil + + private var logOptions: AutoLogPurchaseRevenueOptions = [] + + /// Constants used in method channel for Flutter calls. + private let logSubscriptionsKey = "logSubscriptions" + private let logInAppsKey = "logInApps" + private let sandboxKey = "sandbox" + + /// Private constructor, used to prevent direct instantiation of this class and ensure singleton behaviour. + private override init() {} + + /// Mandatory method needed to register the plugin with iOS part of Flutter app. + public static func register(with registrar: FlutterPluginRegistrar) { + /// Create a new method channel with the registrar. + shared.methodChannel = FlutterMethodChannel(name: AF_PURCHASE_CONNECTOR_CHANNEL, binaryMessenger: registrar.messenger()) + shared.methodChannel!.setMethodCallHandler(shared.methodCallHandler) + + } + + /// Method called when a Flutter method call occurs. It handles and routes flutter method invocations. + private func methodCallHandler(call: FlutterMethodCall, result: @escaping FlutterResult) { + switch(call.method) { + /// Match incoming flutter calls from Dart side to its corresponding native method. + case "configure": + configure(call: call, result: result) + case "startObservingTransactions": + startObservingTransactions(result: result) + case "stopObservingTransactions": + stopObservingTransactions(result: result) + default: + /// This condition handles an error scenario where the method call doesn't match any predefined cases. + result(FlutterMethodNotImplemented) + } + } + + /// This method corresponds to the 'configure' call from Flutter and initiates the PurchaseConnector instance. + private func configure(call: FlutterMethodCall, result: @escaping FlutterResult) { + /// Perform a guard check to ensure that we do not reconfigure an existing connector. + guard connector == nil else { + result(FlutterError(code: "401", message: "Connector already configured", details: nil)) + return + } + + /// Obtain a shared instance of PurchaseConnector + connector = PurchaseConnector.shared() + + /// Extract all the required parameters from Flutter arguments + let arguments = call.arguments as? [String: Any] + let logSubscriptions = arguments?[logSubscriptionsKey] as? Bool ?? false + let logInApps = arguments?[logInAppsKey] as? Bool ?? false + let sandbox = arguments?[sandboxKey] as? Bool ?? false + + /// Define an options variable to manage enabled options. + var options: AutoLogPurchaseRevenueOptions = [] + + /// Based on the arguments, insert the corresponding options. + if logSubscriptions { + options.insert(.autoRenewableSubscriptions) + } + if logInApps { + options.insert(.inAppPurchases) + } + + /// Update the PurchaseConnector instance with these options. + connector!.autoLogPurchaseRevenue = options + logOptions = options + connector!.isSandbox = sandbox + + /// Report a successful operation back to Dart. + result(nil) + } + + /// This function starts the process of observing transactions in the iOS App Store. + private func startObservingTransactions(result: @escaping FlutterResult) { + connectorOperation(result: result) { connector in + // From the docs: If you called stopObservingTransactions API, set the autoLogPurchaseRevenue value before you call startObservingTransactions next time. + connector.autoLogPurchaseRevenue = self.logOptions + connector.startObservingTransactions() + + } + } + + /// This function stops the process of observing transactions in the iOS App Store. + private func stopObservingTransactions(result: @escaping FlutterResult) { + connectorOperation(result: result) { connector in + connector.stopObservingTransactions() + } + } + + /// Helper function used to extract common logic for operations on the connector. + private func connectorOperation(result: @escaping FlutterResult, operation: @escaping ((PurchaseConnector) -> Void)) { + guard connector != nil else { + result(FlutterError(code: "404", message: "Connector not configured, did you called `configure` first?", details: nil)) + return + } + /// Perform the operation with the current connector + operation(connector!) + + result(nil) + } +} + +/// Extension enabling `PurchaseConnectorPlugin` to conform to `PurchaseRevenueDelegate` +extension PurchaseConnectorPlugin: PurchaseRevenueDelegate { + /// Implementation of the `didReceivePurchaseRevenueValidationInfo` delegate method. + /// When the validation info comes back after a purchase, it is reported back to the Flutter via the method channel. + public func didReceivePurchaseRevenueValidationInfo(_ validationInfo: [AnyHashable : Any]?, error: Error?) { + var resMap: [AnyHashable : Any?] = [ + "validationInfo": validationInfo, + "error" : error?.asDictionary + ] + methodChannel?.invokeMethod("didReceivePurchaseRevenueValidationInfo", arguments: resMap) + } +} + +/// Extending `Error` to have a dictionary representation function. `asDictionary` will turn the current error instance into a dictionary containing `localizedDescription`, `domain` and `code` properties. +extension Error { + var asDictionary: [String: Any] { + var errorMap: [String: Any] = ["localizedDescription": self.localizedDescription] + if let nsError = self as? NSError { + errorMap["domain"] = nsError.domain + errorMap["code"] = nsError.code + } + return errorMap + } +} diff --git a/ios/appsflyer_sdk.podspec b/ios/appsflyer_sdk.podspec index 2c063dd5..53440a63 100644 --- a/ios/appsflyer_sdk.podspec +++ b/ios/appsflyer_sdk.podspec @@ -1,25 +1,35 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# Pod::Spec.new do |s| s.name = 'appsflyer_sdk' s.version = '6.14.3' s.summary = 'AppsFlyer Integration for Flutter' - s.description = <<-DESC -AppsFlyer is the market leader in mobile advertising attribution & analytics, helping marketers to pinpoint their targeting, optimize their ad spend and boost their ROI. - DESC + s.description = 'AppsFlyer is the market leader in mobile advertising attribution & analytics, helping marketers to pinpoint their targeting, optimize their ad spend and boost their ROI.' s.homepage = 'https://github.com/AppsFlyerSDK/flutter_appsflyer_sdk' s.license = { :type => 'MIT', :file => '../LICENSE' } s.author = { "Appsflyer" => "build@appsflyer.com" } s.source = { :git => "https://github.com/AppsFlyerSDK/flutter_appsflyer_sdk.git", :tag => s.version.to_s } - - + s.ios.deployment_target = '12.0' s.requires_arc = true s.static_framework = true + if defined?($AppsFlyerPurchaseConnector) + s.default_subspecs = 'Core', 'PurchaseConnector' # add this line + else + s.default_subspecs = 'Core' # add this line + end + + s.subspec 'Core' do |ss| + ss.source_files = 'Classes/**/*' + ss.public_header_files = 'Classes/**/*.h' + ss.dependency 'Flutter' + ss.ios.dependency 'AppsFlyerFramework','6.14.3' + end + + s.subspec 'PurchaseConnector' do |ss| + ss.dependency 'Flutter' + ss.ios.dependency 'PurchaseConnector', '6.14.3' + ss.source_files = 'PurchaseConnector/**/*' + ss.public_header_files = 'PurchaseConnector/**/*.h' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - s.ios.dependency 'AppsFlyerFramework','6.14.3' -end + ss.pod_target_xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) ENABLE_PURCHASE_CONNECTOR=1' } + end +end \ No newline at end of file diff --git a/lib/appsflyer_sdk.dart b/lib/appsflyer_sdk.dart index 80863059..88865558 100644 --- a/lib/appsflyer_sdk.dart +++ b/lib/appsflyer_sdk.dart @@ -7,6 +7,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:json_annotation/json_annotation.dart'; import 'src/callbacks.dart'; @@ -16,5 +17,17 @@ part 'src/appsflyer_options.dart'; part 'src/appsflyer_sdk.dart'; part 'src/udl/deep_link_result.dart'; part 'src/udl/deeplink.dart'; +part 'src/purchase_connector/purchase_connector.dart'; +part 'src/purchase_connector/connector_callbacks.dart'; +part 'src/purchase_connector/missing_configuration_exception.dart'; +part 'src/purchase_connector/purchase_connector_configuration.dart'; +part 'src/purchase_connector/models/subscription_purchase.dart'; +part 'src/purchase_connector/models/in_app_purchase_validation_result.dart'; +part 'src/purchase_connector/models/product_purchase.dart'; +part 'src/purchase_connector/models/subscription_validation_result.dart'; +part 'src/purchase_connector/models/validation_failure_data.dart'; +part 'src/purchase_connector/models/jvm_throwable.dart'; +part 'src/purchase_connector/models/ios_error.dart'; part 'src/appsflyer_consent.dart'; part 'src/appsflyer_request_listener.dart'; +part 'appsflyer_sdk.g.dart'; \ No newline at end of file diff --git a/lib/appsflyer_sdk.g.dart b/lib/appsflyer_sdk.g.dart new file mode 100644 index 00000000..64ce3e8a --- /dev/null +++ b/lib/appsflyer_sdk.g.dart @@ -0,0 +1,431 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'appsflyer_sdk.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SubscriptionPurchase _$SubscriptionPurchaseFromJson( + Map json) => + SubscriptionPurchase( + json['acknowledgementState'] as String, + json['canceledStateContext'] == null + ? null + : CanceledStateContext.fromJson( + json['canceledStateContext'] as Map), + json['externalAccountIdentifiers'] == null + ? null + : ExternalAccountIdentifiers.fromJson( + json['externalAccountIdentifiers'] as Map), + json['kind'] as String, + json['latestOrderId'] as String, + (json['lineItems'] as List) + .map((e) => + SubscriptionPurchaseLineItem.fromJson(e as Map)) + .toList(), + json['linkedPurchaseToken'] as String?, + json['pausedStateContext'] == null + ? null + : PausedStateContext.fromJson( + json['pausedStateContext'] as Map), + json['regionCode'] as String, + json['startTime'] as String, + json['subscribeWithGoogleInfo'] == null + ? null + : SubscribeWithGoogleInfo.fromJson( + json['subscribeWithGoogleInfo'] as Map), + json['subscriptionState'] as String, + json['testPurchase'] == null + ? null + : TestPurchase.fromJson(json['testPurchase'] as Map), + ); + +Map _$SubscriptionPurchaseToJson( + SubscriptionPurchase instance) => + { + 'acknowledgementState': instance.acknowledgementState, + 'canceledStateContext': instance.canceledStateContext, + 'externalAccountIdentifiers': instance.externalAccountIdentifiers, + 'kind': instance.kind, + 'latestOrderId': instance.latestOrderId, + 'lineItems': instance.lineItems, + 'linkedPurchaseToken': instance.linkedPurchaseToken, + 'pausedStateContext': instance.pausedStateContext, + 'regionCode': instance.regionCode, + 'startTime': instance.startTime, + 'subscribeWithGoogleInfo': instance.subscribeWithGoogleInfo, + 'subscriptionState': instance.subscriptionState, + 'testPurchase': instance.testPurchase, + }; + +CanceledStateContext _$CanceledStateContextFromJson( + Map json) => + CanceledStateContext( + json['developerInitiatedCancellation'] == null + ? null + : DeveloperInitiatedCancellation.fromJson( + json['developerInitiatedCancellation'] as Map), + json['replacementCancellation'] == null + ? null + : ReplacementCancellation.fromJson( + json['replacementCancellation'] as Map), + json['systemInitiatedCancellation'] == null + ? null + : SystemInitiatedCancellation.fromJson( + json['systemInitiatedCancellation'] as Map), + json['userInitiatedCancellation'] == null + ? null + : UserInitiatedCancellation.fromJson( + json['userInitiatedCancellation'] as Map), + ); + +Map _$CanceledStateContextToJson( + CanceledStateContext instance) => + { + 'developerInitiatedCancellation': instance.developerInitiatedCancellation, + 'replacementCancellation': instance.replacementCancellation, + 'systemInitiatedCancellation': instance.systemInitiatedCancellation, + 'userInitiatedCancellation': instance.userInitiatedCancellation, + }; + +DeveloperInitiatedCancellation _$DeveloperInitiatedCancellationFromJson( + Map json) => + DeveloperInitiatedCancellation(); + +Map _$DeveloperInitiatedCancellationToJson( + DeveloperInitiatedCancellation instance) => + {}; + +ReplacementCancellation _$ReplacementCancellationFromJson( + Map json) => + ReplacementCancellation(); + +Map _$ReplacementCancellationToJson( + ReplacementCancellation instance) => + {}; + +SystemInitiatedCancellation _$SystemInitiatedCancellationFromJson( + Map json) => + SystemInitiatedCancellation(); + +Map _$SystemInitiatedCancellationToJson( + SystemInitiatedCancellation instance) => + {}; + +UserInitiatedCancellation _$UserInitiatedCancellationFromJson( + Map json) => + UserInitiatedCancellation( + json['cancelSurveyResult'] == null + ? null + : CancelSurveyResult.fromJson( + json['cancelSurveyResult'] as Map), + json['cancelTime'] as String, + ); + +Map _$UserInitiatedCancellationToJson( + UserInitiatedCancellation instance) => + { + 'cancelSurveyResult': instance.cancelSurveyResult, + 'cancelTime': instance.cancelTime, + }; + +CancelSurveyResult _$CancelSurveyResultFromJson(Map json) => + CancelSurveyResult( + json['reason'] as String, + json['reasonUserInput'] as String, + ); + +Map _$CancelSurveyResultToJson(CancelSurveyResult instance) => + { + 'reason': instance.reason, + 'reasonUserInput': instance.reasonUserInput, + }; + +ExternalAccountIdentifiers _$ExternalAccountIdentifiersFromJson( + Map json) => + ExternalAccountIdentifiers( + json['externalAccountId'] as String, + json['obfuscatedExternalAccountId'] as String, + json['obfuscatedExternalProfileId'] as String, + ); + +Map _$ExternalAccountIdentifiersToJson( + ExternalAccountIdentifiers instance) => + { + 'externalAccountId': instance.externalAccountId, + 'obfuscatedExternalAccountId': instance.obfuscatedExternalAccountId, + 'obfuscatedExternalProfileId': instance.obfuscatedExternalProfileId, + }; + +SubscriptionPurchaseLineItem _$SubscriptionPurchaseLineItemFromJson( + Map json) => + SubscriptionPurchaseLineItem( + json['autoRenewingPlan'] == null + ? null + : AutoRenewingPlan.fromJson( + json['autoRenewingPlan'] as Map), + json['deferredItemReplacement'] == null + ? null + : DeferredItemReplacement.fromJson( + json['deferredItemReplacement'] as Map), + json['expiryTime'] as String, + json['offerDetails'] == null + ? null + : OfferDetails.fromJson(json['offerDetails'] as Map), + json['prepaidPlan'] == null + ? null + : PrepaidPlan.fromJson(json['prepaidPlan'] as Map), + json['productId'] as String, + ); + +Map _$SubscriptionPurchaseLineItemToJson( + SubscriptionPurchaseLineItem instance) => + { + 'autoRenewingPlan': instance.autoRenewingPlan, + 'deferredItemReplacement': instance.deferredItemReplacement, + 'expiryTime': instance.expiryTime, + 'offerDetails': instance.offerDetails, + 'prepaidPlan': instance.prepaidPlan, + 'productId': instance.productId, + }; + +OfferDetails _$OfferDetailsFromJson(Map json) => OfferDetails( + (json['offerTags'] as List?)?.map((e) => e as String).toList(), + json['basePlanId'] as String, + json['offerId'] as String?, + ); + +Map _$OfferDetailsToJson(OfferDetails instance) => + { + 'offerTags': instance.offerTags, + 'basePlanId': instance.basePlanId, + 'offerId': instance.offerId, + }; + +AutoRenewingPlan _$AutoRenewingPlanFromJson(Map json) => + AutoRenewingPlan( + json['autoRenewEnabled'] as bool?, + json['priceChangeDetails'] == null + ? null + : SubscriptionItemPriceChangeDetails.fromJson( + json['priceChangeDetails'] as Map), + ); + +Map _$AutoRenewingPlanToJson(AutoRenewingPlan instance) => + { + 'autoRenewEnabled': instance.autoRenewEnabled, + 'priceChangeDetails': instance.priceChangeDetails, + }; + +SubscriptionItemPriceChangeDetails _$SubscriptionItemPriceChangeDetailsFromJson( + Map json) => + SubscriptionItemPriceChangeDetails( + json['expectedNewPriceChargeTime'] as String, + json['newPrice'] == null + ? null + : Money.fromJson(json['newPrice'] as Map), + json['priceChangeMode'] as String, + json['priceChangeState'] as String, + ); + +Map _$SubscriptionItemPriceChangeDetailsToJson( + SubscriptionItemPriceChangeDetails instance) => + { + 'expectedNewPriceChargeTime': instance.expectedNewPriceChargeTime, + 'newPrice': instance.newPrice, + 'priceChangeMode': instance.priceChangeMode, + 'priceChangeState': instance.priceChangeState, + }; + +Money _$MoneyFromJson(Map json) => Money( + json['currencyCode'] as String, + (json['nanos'] as num).toInt(), + (json['units'] as num).toInt(), + ); + +Map _$MoneyToJson(Money instance) => { + 'currencyCode': instance.currencyCode, + 'nanos': instance.nanos, + 'units': instance.units, + }; + +DeferredItemReplacement _$DeferredItemReplacementFromJson( + Map json) => + DeferredItemReplacement( + json['productId'] as String, + ); + +Map _$DeferredItemReplacementToJson( + DeferredItemReplacement instance) => + { + 'productId': instance.productId, + }; + +PrepaidPlan _$PrepaidPlanFromJson(Map json) => PrepaidPlan( + json['allowExtendAfterTime'] as String?, + ); + +Map _$PrepaidPlanToJson(PrepaidPlan instance) => + { + 'allowExtendAfterTime': instance.allowExtendAfterTime, + }; + +PausedStateContext _$PausedStateContextFromJson(Map json) => + PausedStateContext( + json['autoResumeTime'] as String, + ); + +Map _$PausedStateContextToJson(PausedStateContext instance) => + { + 'autoResumeTime': instance.autoResumeTime, + }; + +SubscribeWithGoogleInfo _$SubscribeWithGoogleInfoFromJson( + Map json) => + SubscribeWithGoogleInfo( + json['emailAddress'] as String, + json['familyName'] as String, + json['givenName'] as String, + json['profileId'] as String, + json['profileName'] as String, + ); + +Map _$SubscribeWithGoogleInfoToJson( + SubscribeWithGoogleInfo instance) => + { + 'emailAddress': instance.emailAddress, + 'familyName': instance.familyName, + 'givenName': instance.givenName, + 'profileId': instance.profileId, + 'profileName': instance.profileName, + }; + +TestPurchase _$TestPurchaseFromJson(Map json) => + TestPurchase(); + +Map _$TestPurchaseToJson(TestPurchase instance) => + {}; + +InAppPurchaseValidationResult _$InAppPurchaseValidationResultFromJson( + Map json) => + InAppPurchaseValidationResult( + json['success'] as bool, + json['productPurchase'] == null + ? null + : ProductPurchase.fromJson( + json['productPurchase'] as Map), + json['failureData'] == null + ? null + : ValidationFailureData.fromJson( + json['failureData'] as Map), + ); + +Map _$InAppPurchaseValidationResultToJson( + InAppPurchaseValidationResult instance) => + { + 'success': instance.success, + 'productPurchase': instance.productPurchase, + 'failureData': instance.failureData, + }; + +ProductPurchase _$ProductPurchaseFromJson(Map json) => + ProductPurchase( + json['kind'] as String, + json['purchaseTimeMillis'] as String, + (json['purchaseState'] as num).toInt(), + (json['consumptionState'] as num).toInt(), + json['developerPayload'] as String, + json['orderId'] as String, + (json['purchaseType'] as num).toInt(), + (json['acknowledgementState'] as num).toInt(), + json['purchaseToken'] as String, + json['productId'] as String, + (json['quantity'] as num).toInt(), + json['obfuscatedExternalAccountId'] as String, + json['obfuscatedExternalProfileId'] as String, + json['regionCode'] as String, + ); + +Map _$ProductPurchaseToJson(ProductPurchase instance) => + { + 'kind': instance.kind, + 'purchaseTimeMillis': instance.purchaseTimeMillis, + 'purchaseState': instance.purchaseState, + 'consumptionState': instance.consumptionState, + 'developerPayload': instance.developerPayload, + 'orderId': instance.orderId, + 'purchaseType': instance.purchaseType, + 'acknowledgementState': instance.acknowledgementState, + 'purchaseToken': instance.purchaseToken, + 'productId': instance.productId, + 'quantity': instance.quantity, + 'obfuscatedExternalAccountId': instance.obfuscatedExternalAccountId, + 'obfuscatedExternalProfileId': instance.obfuscatedExternalProfileId, + 'regionCode': instance.regionCode, + }; + +SubscriptionValidationResult _$SubscriptionValidationResultFromJson( + Map json) => + SubscriptionValidationResult( + json['success'] as bool, + json['subscriptionPurchase'] == null + ? null + : SubscriptionPurchase.fromJson( + json['subscriptionPurchase'] as Map), + json['failureData'] == null + ? null + : ValidationFailureData.fromJson( + json['failureData'] as Map), + ); + +Map _$SubscriptionValidationResultToJson( + SubscriptionValidationResult instance) => + { + 'success': instance.success, + 'subscriptionPurchase': instance.subscriptionPurchase, + 'failureData': instance.failureData, + }; + +ValidationFailureData _$ValidationFailureDataFromJson( + Map json) => + ValidationFailureData( + (json['status'] as num).toInt(), + json['description'] as String, + ); + +Map _$ValidationFailureDataToJson( + ValidationFailureData instance) => + { + 'status': instance.status, + 'description': instance.description, + }; + +JVMThrowable _$JVMThrowableFromJson(Map json) => JVMThrowable( + json['type'] as String, + json['message'] as String, + json['stacktrace'] as String, + json['cause'] == null + ? null + : JVMThrowable.fromJson(json['cause'] as Map), + ); + +Map _$JVMThrowableToJson(JVMThrowable instance) => + { + 'type': instance.type, + 'message': instance.message, + 'stacktrace': instance.stacktrace, + 'cause': instance.cause, + }; + +IosError _$IosErrorFromJson(Map json) => IosError( + json['localizedDescription'] as String, + json['domain'] as String, + (json['code'] as num).toInt(), + ); + +Map _$IosErrorToJson(IosError instance) => { + 'localizedDescription': instance.localizedDescription, + 'domain': instance.domain, + 'code': instance.code, + }; diff --git a/lib/src/appsflyer_constants.dart b/lib/src/appsflyer_constants.dart index 0ee155e0..f6a2a7c2 100644 --- a/lib/src/appsflyer_constants.dart +++ b/lib/src/appsflyer_constants.dart @@ -6,6 +6,10 @@ enum EmailCryptType { } class AppsflyerConstants { + + static const String RE_CONFIGURE_ERROR_MSG = "[PurchaseConnector] Re configure instance is not permitted. Returned the existing instance"; + static const String MISSING_CONFIGURATION_EXCEPTION_MSG = "Could not create an instance without configuration"; + static const String AF_DEV_KEY = "afDevKey"; static const String AF_APP_Id = "afAppId"; static const String AF_IS_DEBUG = "isDebug"; @@ -22,6 +26,7 @@ class AppsflyerConstants { static const String AF_EVENTS_CHANNEL = "af-events"; static const String AF_METHOD_CHANNEL = "af-api"; + static const String AF_PURCHASE_CONNECTOR_CHANNEL = "af-purchase-connector"; static const String AF_VALIDATE_PURCHASE = "validatePurchase"; static const String APP_INVITE_ONE_LINK = "appInviteOneLink"; @@ -29,4 +34,25 @@ class AppsflyerConstants { static const String DISABLE_COLLECT_ASA = "disableCollectASA"; static const String DISABLE_ADVERTISING_IDENTIFIER = "disableAdvertisingIdentifier"; + + // Adding method constants + static const String SUBSCRIPTION_PURCHASE_VALIDATION_RESULT_LISTENER_ON_RESPONSE = + "SubscriptionPurchaseValidationResultListener:onResponse"; + static const String SUBSCRIPTION_PURCHASE_VALIDATION_RESULT_LISTENER_ON_FAILURE = + "SubscriptionPurchaseValidationResultListener:onFailure"; + static const String IN_APP_VALIDATION_RESULT_LISTENER_ON_RESPONSE = + "InAppValidationResultListener:onResponse"; + static const String IN_APP_VALIDATION_RESULT_LISTENER_ON_FAILURE = + "InAppValidationResultListener:onFailure"; + static const String DID_RECEIVE_PURCHASE_REVENUE_VALIDATION_INFO = + "didReceivePurchaseRevenueValidationInfo"; + +// Adding key constants + static const String RESULT = "result"; + static const String ERROR = "error"; + static const String VALIDATION_INFO = "validationInfo"; + static const String CONFIGURE_KEY = "configure"; + static const String LOG_SUBS_KEY = "logSubscriptions"; + static const String LOG_IN_APP_KEY = "logInApps"; + static const String SANDBOX_KEY = "sandbox"; } diff --git a/lib/src/purchase_connector/connector_callbacks.dart b/lib/src/purchase_connector/connector_callbacks.dart new file mode 100644 index 00000000..c908cfea --- /dev/null +++ b/lib/src/purchase_connector/connector_callbacks.dart @@ -0,0 +1,18 @@ +part of appsflyer_sdk; + +/// Type definition for a general purpose listener. +typedef PurchaseConnectorListener = Function(dynamic); +/// Type definition for a listener which gets called when the `PurchaseConnectorImpl` receives purchase revenue validation info for iOS. +typedef DidReceivePurchaseRevenueValidationInfo = Function( + Map? validationInfo, IosError? error); +/// Invoked when a 200 OK response is received from the server. +/// Note: An INVALID purchase is considered to be a successful response and will also be returned by this callback. +/// +/// [result] Server's response. +typedef OnResponse = Function(Map? result); + +/// Invoked when a network exception occurs or a non 200/OK response is received from the server. +/// +/// [result] The server's response. +/// [error] The exception that occurred during execution. +typedef OnFailure = Function(String result, JVMThrowable? error); diff --git a/lib/src/purchase_connector/missing_configuration_exception.dart b/lib/src/purchase_connector/missing_configuration_exception.dart new file mode 100644 index 00000000..eae23676 --- /dev/null +++ b/lib/src/purchase_connector/missing_configuration_exception.dart @@ -0,0 +1,11 @@ +part of appsflyer_sdk; +/// Exception thrown when the configuration is missing. +class MissingConfigurationException implements Exception { + final String message; + + MissingConfigurationException( + {this.message = AppsflyerConstants.MISSING_CONFIGURATION_EXCEPTION_MSG}); + + @override + String toString() => 'ConfigurationException: $message'; +} diff --git a/lib/src/purchase_connector/models/in_app_purchase_validation_result.dart b/lib/src/purchase_connector/models/in_app_purchase_validation_result.dart new file mode 100644 index 00000000..6f8f909f --- /dev/null +++ b/lib/src/purchase_connector/models/in_app_purchase_validation_result.dart @@ -0,0 +1,22 @@ +part of appsflyer_sdk; + +@JsonSerializable() +class InAppPurchaseValidationResult { + + bool success; + ProductPurchase? productPurchase; + ValidationFailureData? failureData; + + InAppPurchaseValidationResult( + this.success, + this.productPurchase, + this.failureData + ); + + + + factory InAppPurchaseValidationResult.fromJson(Map json) => _$InAppPurchaseValidationResultFromJson(json); + + Map toJson() => _$InAppPurchaseValidationResultToJson(this); + +} \ No newline at end of file diff --git a/lib/src/purchase_connector/models/ios_error.dart b/lib/src/purchase_connector/models/ios_error.dart new file mode 100644 index 00000000..aa671066 --- /dev/null +++ b/lib/src/purchase_connector/models/ios_error.dart @@ -0,0 +1,15 @@ +part of appsflyer_sdk; + +@JsonSerializable() +class IosError{ + String localizedDescription; + String domain; + int code; + + + IosError(this.localizedDescription, this.domain, this.code); + + factory IosError.fromJson(Map json) => _$IosErrorFromJson(json); + + Map toJson() => _$IosErrorToJson(this); +} \ No newline at end of file diff --git a/lib/src/purchase_connector/models/jvm_throwable.dart b/lib/src/purchase_connector/models/jvm_throwable.dart new file mode 100644 index 00000000..04fb07e5 --- /dev/null +++ b/lib/src/purchase_connector/models/jvm_throwable.dart @@ -0,0 +1,16 @@ +part of appsflyer_sdk; + +@JsonSerializable() +class JVMThrowable{ + String type; + String message; + String stacktrace; + JVMThrowable? cause; + + JVMThrowable(this.type, this.message, this.stacktrace, this.cause); + + factory JVMThrowable.fromJson(Map json) => _$JVMThrowableFromJson(json); + + Map toJson() => _$JVMThrowableToJson(this); + +} \ No newline at end of file diff --git a/lib/src/purchase_connector/models/product_purchase.dart b/lib/src/purchase_connector/models/product_purchase.dart new file mode 100644 index 00000000..e6e3ceb6 --- /dev/null +++ b/lib/src/purchase_connector/models/product_purchase.dart @@ -0,0 +1,44 @@ +part of appsflyer_sdk; + +@JsonSerializable() +class ProductPurchase { + + String kind; + String purchaseTimeMillis; + int purchaseState; + int consumptionState; + String developerPayload; + String orderId; + int purchaseType; + int acknowledgementState; + String purchaseToken; + String productId; + int quantity; + String obfuscatedExternalAccountId; + String obfuscatedExternalProfileId; + String regionCode; + + ProductPurchase( + this.kind, + this.purchaseTimeMillis, + this.purchaseState, + this.consumptionState, + this.developerPayload, + this.orderId, + this.purchaseType, + this.acknowledgementState, + this.purchaseToken, + this.productId, + this.quantity, + this.obfuscatedExternalAccountId, + this.obfuscatedExternalProfileId, + this.regionCode + ); + + + + factory ProductPurchase.fromJson(Map json) => _$ProductPurchaseFromJson(json); + + Map toJson() => _$ProductPurchaseToJson(this); + +} \ No newline at end of file diff --git a/lib/src/purchase_connector/models/subscription_purchase.dart b/lib/src/purchase_connector/models/subscription_purchase.dart new file mode 100644 index 00000000..55ede1e6 --- /dev/null +++ b/lib/src/purchase_connector/models/subscription_purchase.dart @@ -0,0 +1,343 @@ +part of appsflyer_sdk; + +@JsonSerializable() +class SubscriptionPurchase { + + String acknowledgementState; + CanceledStateContext? canceledStateContext; + ExternalAccountIdentifiers? externalAccountIdentifiers; + String kind; + String latestOrderId; + List lineItems; + String? linkedPurchaseToken; + PausedStateContext? pausedStateContext; + String regionCode; + String startTime; + SubscribeWithGoogleInfo? subscribeWithGoogleInfo; + String subscriptionState; + TestPurchase? testPurchase; + + SubscriptionPurchase( + this.acknowledgementState, + this.canceledStateContext, + this.externalAccountIdentifiers, + this.kind, + this.latestOrderId, + this.lineItems, + this.linkedPurchaseToken, + this.pausedStateContext, + this.regionCode, + this.startTime, + this.subscribeWithGoogleInfo, + this.subscriptionState, + this.testPurchase + ); + + + + factory SubscriptionPurchase.fromJson(Map json) => _$SubscriptionPurchaseFromJson(json); + + Map toJson() => _$SubscriptionPurchaseToJson(this); + +} + + +@JsonSerializable() +class CanceledStateContext { + + DeveloperInitiatedCancellation? developerInitiatedCancellation; + ReplacementCancellation? replacementCancellation; + SystemInitiatedCancellation? systemInitiatedCancellation; + UserInitiatedCancellation? userInitiatedCancellation; + + CanceledStateContext( + this.developerInitiatedCancellation, + this.replacementCancellation, + this.systemInitiatedCancellation, + this.userInitiatedCancellation + ); + + + + factory CanceledStateContext.fromJson(Map json) => _$CanceledStateContextFromJson(json); + + Map toJson() => _$CanceledStateContextToJson(this); + +} + +@JsonSerializable() +class DeveloperInitiatedCancellation{ + DeveloperInitiatedCancellation(); + factory DeveloperInitiatedCancellation.fromJson(Map json) => _$DeveloperInitiatedCancellationFromJson(json); + + Map toJson() => _$DeveloperInitiatedCancellationToJson(this); +} + +@JsonSerializable() +class ReplacementCancellation{ + ReplacementCancellation(); + factory ReplacementCancellation.fromJson(Map json) => _$ReplacementCancellationFromJson(json); + + Map toJson() => _$ReplacementCancellationToJson(this); +} + +@JsonSerializable() +class SystemInitiatedCancellation{ + SystemInitiatedCancellation(); + factory SystemInitiatedCancellation.fromJson(Map json) => _$SystemInitiatedCancellationFromJson(json); + + Map toJson() => _$SystemInitiatedCancellationToJson(this); +} + + +@JsonSerializable() +class UserInitiatedCancellation { + + CancelSurveyResult? cancelSurveyResult; + String cancelTime; + + UserInitiatedCancellation( + this.cancelSurveyResult, + this.cancelTime + ); + + + + factory UserInitiatedCancellation.fromJson(Map json) => _$UserInitiatedCancellationFromJson(json); + + Map toJson() => _$UserInitiatedCancellationToJson(this); + +} + +@JsonSerializable() +class CancelSurveyResult { + + String reason; + String reasonUserInput; + + CancelSurveyResult( + this.reason, + this.reasonUserInput + ); + + + + factory CancelSurveyResult.fromJson(Map json) => _$CancelSurveyResultFromJson(json); + + Map toJson() => _$CancelSurveyResultToJson(this); + +} + +@JsonSerializable() +class ExternalAccountIdentifiers { + + String externalAccountId; + String obfuscatedExternalAccountId; + String obfuscatedExternalProfileId; + + ExternalAccountIdentifiers( + this.externalAccountId, + this.obfuscatedExternalAccountId, + this.obfuscatedExternalProfileId + ); + + + + factory ExternalAccountIdentifiers.fromJson(Map json) => _$ExternalAccountIdentifiersFromJson(json); + + Map toJson() => _$ExternalAccountIdentifiersToJson(this); + +} + +@JsonSerializable() +class SubscriptionPurchaseLineItem { + + AutoRenewingPlan? autoRenewingPlan; + DeferredItemReplacement? deferredItemReplacement; + String expiryTime; + OfferDetails? offerDetails; + PrepaidPlan? prepaidPlan; + String productId; + + SubscriptionPurchaseLineItem( + this.autoRenewingPlan, + this.deferredItemReplacement, + this.expiryTime, + this.offerDetails, + this.prepaidPlan, + this.productId + ); + + + + factory SubscriptionPurchaseLineItem.fromJson(Map json) => _$SubscriptionPurchaseLineItemFromJson(json); + + Map toJson() => _$SubscriptionPurchaseLineItemToJson(this); + +} + +@JsonSerializable() +class OfferDetails { + + List? offerTags; + String basePlanId; + String? offerId; + + OfferDetails( + this.offerTags, + this.basePlanId, + this.offerId + ); + + + + factory OfferDetails.fromJson(Map json) => _$OfferDetailsFromJson(json); + + Map toJson() => _$OfferDetailsToJson(this); + +} + +@JsonSerializable() +class AutoRenewingPlan { + + bool? autoRenewEnabled; + SubscriptionItemPriceChangeDetails? priceChangeDetails; + + AutoRenewingPlan( + this.autoRenewEnabled, + this.priceChangeDetails + ); + + + + factory AutoRenewingPlan.fromJson(Map json) => _$AutoRenewingPlanFromJson(json); + + Map toJson() => _$AutoRenewingPlanToJson(this); + +} + +@JsonSerializable() +class SubscriptionItemPriceChangeDetails { + + String expectedNewPriceChargeTime; + Money? newPrice; + String priceChangeMode; + String priceChangeState; + + SubscriptionItemPriceChangeDetails( + this.expectedNewPriceChargeTime, + this.newPrice, + this.priceChangeMode, + this.priceChangeState + ); + + + + factory SubscriptionItemPriceChangeDetails.fromJson(Map json) => _$SubscriptionItemPriceChangeDetailsFromJson(json); + + Map toJson() => _$SubscriptionItemPriceChangeDetailsToJson(this); + +} + +@JsonSerializable() +class Money { + + String currencyCode; + int nanos; + int units; + + Money( + this.currencyCode, + this.nanos, + this.units + ); + + + + factory Money.fromJson(Map json) => _$MoneyFromJson(json); + + Map toJson() => _$MoneyToJson(this); + +} +@JsonSerializable() +class DeferredItemReplacement { + + String productId; + + DeferredItemReplacement( + this.productId + ); + + + + factory DeferredItemReplacement.fromJson(Map json) => _$DeferredItemReplacementFromJson(json); + + Map toJson() => _$DeferredItemReplacementToJson(this); + +} + +@JsonSerializable() +class PrepaidPlan { + + String? allowExtendAfterTime; + + PrepaidPlan( + this.allowExtendAfterTime + ); + + + + factory PrepaidPlan.fromJson(Map json) => _$PrepaidPlanFromJson(json); + + Map toJson() => _$PrepaidPlanToJson(this); + +} + +@JsonSerializable() +class PausedStateContext { + + String autoResumeTime; + + PausedStateContext( + this.autoResumeTime + ); + + + + factory PausedStateContext.fromJson(Map json) => _$PausedStateContextFromJson(json); + + Map toJson() => _$PausedStateContextToJson(this); + +} +@JsonSerializable() +class SubscribeWithGoogleInfo { + + String emailAddress; + String familyName; + String givenName; + String profileId; + String profileName; + + SubscribeWithGoogleInfo( + this.emailAddress, + this.familyName, + this.givenName, + this.profileId, + this.profileName + ); + + + + factory SubscribeWithGoogleInfo.fromJson(Map json) => _$SubscribeWithGoogleInfoFromJson(json); + + Map toJson() => _$SubscribeWithGoogleInfoToJson(this); + +} + +@JsonSerializable() +class TestPurchase{ + TestPurchase(); + factory TestPurchase.fromJson(Map json) => _$TestPurchaseFromJson(json); + + Map toJson() => _$TestPurchaseToJson(this); +} \ No newline at end of file diff --git a/lib/src/purchase_connector/models/subscription_validation_result.dart b/lib/src/purchase_connector/models/subscription_validation_result.dart new file mode 100644 index 00000000..a94647bd --- /dev/null +++ b/lib/src/purchase_connector/models/subscription_validation_result.dart @@ -0,0 +1,22 @@ +part of appsflyer_sdk; + +@JsonSerializable() +class SubscriptionValidationResult { + + bool success; + SubscriptionPurchase? subscriptionPurchase; + ValidationFailureData? failureData; + + SubscriptionValidationResult( + this.success, + this.subscriptionPurchase, + this.failureData + ); + + + + factory SubscriptionValidationResult.fromJson(Map json) => _$SubscriptionValidationResultFromJson(json); + + Map toJson() => _$SubscriptionValidationResultToJson(this); + +} \ No newline at end of file diff --git a/lib/src/purchase_connector/models/validation_failure_data.dart b/lib/src/purchase_connector/models/validation_failure_data.dart new file mode 100644 index 00000000..57b3d137 --- /dev/null +++ b/lib/src/purchase_connector/models/validation_failure_data.dart @@ -0,0 +1,20 @@ +part of appsflyer_sdk; + +@JsonSerializable() +class ValidationFailureData { + + int status; + String description; + + ValidationFailureData( + this.status, + this.description + ); + + + + factory ValidationFailureData.fromJson(Map json) => _$ValidationFailureDataFromJson(json); + + Map toJson() => _$ValidationFailureDataToJson(this); + +} \ No newline at end of file diff --git a/lib/src/purchase_connector/purchase_connector.dart b/lib/src/purchase_connector/purchase_connector.dart new file mode 100644 index 00000000..4f5acc8c --- /dev/null +++ b/lib/src/purchase_connector/purchase_connector.dart @@ -0,0 +1,268 @@ +part of appsflyer_sdk; + + +/// Interface representing a purchase connector. +abstract class PurchaseConnector { + + /// Starts observing transactions. + void startObservingTransactions(); + + /// Stops observing transactions. + void stopObservingTransactions(); + + /// Sets the listener for Android subscription validation results. + /// + /// [onResponse] Function to be executed when a successful response is received. + /// [onFailure] Function to be executed when a failure occurs (network exception or non 200/OK response from the server). + void setSubscriptionValidationResultListener( + OnResponse? onResponse, + OnFailure? onFailure); + + /// Sets the listener for Android in-app validation results. + /// + /// [onResponse] Function to be executed when a successful response is received. + /// [onFailure] Function to be executed when a failure occurs (network exception or non 200/OK response from the server). + + void setInAppValidationResultListener( + OnResponse? onResponse, + OnFailure? onFailure); + + /// Sets the listener for iOS subscription and in-app validation results. + /// Parameter: + /// [callback] the function to be executed when `DidReceivePurchaseRevenueValidationInfo` is called. + void setDidReceivePurchaseRevenueValidationInfo( + DidReceivePurchaseRevenueValidationInfo? callback); + + /// Creates a new PurchaseConnector instance. + /// Parameter: + /// [config] the configuration to be used when creating a new `PurchaseConnector` instance. + factory PurchaseConnector({PurchaseConnectorConfiguration? config}) => + _PurchaseConnectorImpl(config: config); +} + + +/// The implementation of the PurchaseConnector. +/// +/// This class is responsible for establishing a connection with Appsflyer purchase connector, +/// starting/stopping observing transactions, setting listeners for various validation results. +class _PurchaseConnectorImpl implements PurchaseConnector { + + /// Singleton instance of the PurchaseConnectorImpl. + static _PurchaseConnectorImpl? _instance; + + /// Method channel to communicate with the Appsflyer Purchase Connector. + final MethodChannel _methodChannel; + + /// Response handler for SubscriptionValidationResult (Android). + OnResponse? _arsOnResponse; + /// Failure handler for SubscriptionValidationResult (Android). + OnFailure? _arsOnFailure; + /// Response handler for InAppPurchaseValidationResult (Android). + OnResponse? _viapOnResponse; + /// Failure handler for InAppPurchaseValidationResult (Android). + OnFailure? _viapOnFailure; + /// Callback handler for receiving validation info for iOS. + DidReceivePurchaseRevenueValidationInfo? + _didReceivePurchaseRevenueValidationInfo; + + /// Internal constructor. Initializes the instance and sets up method call handler. + _PurchaseConnectorImpl._internal( + this._methodChannel, PurchaseConnectorConfiguration config) { + _methodChannel.setMethodCallHandler(_methodCallHandler); + _methodChannel.invokeMethod(AppsflyerConstants.CONFIGURE_KEY, { + AppsflyerConstants.LOG_SUBS_KEY: config.logSubscriptions, + AppsflyerConstants.LOG_IN_APP_KEY: config.logInApps, + AppsflyerConstants.SANDBOX_KEY: config.sandbox, + }); + } + + /// Factory constructor. + /// + /// This factory ensures that only a single instance of `PurchaseConnectorImpl` is used throughout the program + /// by implementing the Singleton design pattern. If an instance already exists, it's returned. + /// + /// The [config] parameter is optional and is used only when creating the first instance of `PurchaseConnectorImpl`. + /// Once an instance is created, the same instance will be returned in subsequent calls, and the [config] + /// parameter will be ignored. Thus, it's valid to call this factory without a config if an instance already exists. + /// + /// If there is no existing instance and the [config] is not provided, a `MissingConfigurationException` will be thrown. + factory _PurchaseConnectorImpl({PurchaseConnectorConfiguration? config}) { + if (_instance == null && config == null) { + // no instance exist and config not provided. We Can't create instance without config + throw MissingConfigurationException(); + } else if (_instance == null && config != null) { + // no existing instance. Create new instance and apply config + MethodChannel methodChannel = + const MethodChannel(AppsflyerConstants.AF_PURCHASE_CONNECTOR_CHANNEL); + _instance = _PurchaseConnectorImpl._internal(methodChannel, config); + } else if (_instance != null && config != null) { + debugPrint(AppsflyerConstants.RE_CONFIGURE_ERROR_MSG); + } + return _instance!; + } + + /// Starts observing the transactions. + @override + void startObservingTransactions() { + _methodChannel.invokeMethod("startObservingTransactions"); + } + + /// Stops observing the transactions. + @override + void stopObservingTransactions() { + _methodChannel.invokeMethod("stopObservingTransactions"); + } + + /// Sets the function to be executed when iOS validation info is received. + @override + void setDidReceivePurchaseRevenueValidationInfo( + DidReceivePurchaseRevenueValidationInfo? callback) { + _didReceivePurchaseRevenueValidationInfo = callback; + } + + /// Sets the listener for Android in-app validation results. + /// + /// [onResponse] Function to be executed when a successful response is received. + /// [onFailure] Function to be executed when a failure occurs (network exception or non 200/OK response from the server). + @override + void setInAppValidationResultListener( + OnResponse? onResponse, + OnFailure? onFailure) { + _viapOnResponse = onResponse; + _viapOnFailure = onFailure; + } + + /// Sets the listener for Android subscription validation results. + /// + /// [onResponse] Function to be executed when a successful response is received. + /// [onFailure] Function to be executed when a failure occurs (network exception or non 200/OK response from the server). + @override + void setSubscriptionValidationResultListener( + OnResponse? onResponse, + OnFailure? onFailure) { + _arsOnResponse = onResponse; + _arsOnFailure = onFailure; + } + + /// Method call handler for different operations. Called by the _methodChannel. + Future _methodCallHandler(MethodCall call) async { + switch (call.method) { + case AppsflyerConstants + .SUBSCRIPTION_PURCHASE_VALIDATION_RESULT_LISTENER_ON_RESPONSE: + _handleSubscriptionPurchaseValidationResultListenerOnResponse( + call.arguments as Map>?); + break; + case AppsflyerConstants + .SUBSCRIPTION_PURCHASE_VALIDATION_RESULT_LISTENER_ON_FAILURE: + _handleSubscriptionPurchaseValidationResultListenerOnFailure( + call.arguments as Map); + break; + case AppsflyerConstants.IN_APP_VALIDATION_RESULT_LISTENER_ON_RESPONSE: + _handleInAppValidationResultListenerOnResponse( + call.arguments as Map>?); + break; + case AppsflyerConstants.IN_APP_VALIDATION_RESULT_LISTENER_ON_FAILURE: + _handleInAppValidationResultListenerOnFailure( + call.arguments as Map); + break; + case AppsflyerConstants.DID_RECEIVE_PURCHASE_REVENUE_VALIDATION_INFO: + _handleDidReceivePurchaseRevenueValidationInfo( + call.arguments as Map); + break; + default: + throw ArgumentError("Method not found."); + } + } + + /// Handles response for the subscription purchase validation result listener. + /// + /// [callbackData] is the callback data expected in the form of a map. + void _handleSubscriptionPurchaseValidationResultListenerOnResponse( + Map>? callbackData) { + _handleValidationResultListenerOnResponse( + callbackData, + _arsOnResponse, + (value) => SubscriptionValidationResult.fromJson(value), + ); + } + + /// Handles response for the in-app validation result listener. + /// + /// [callbackData] is the callback data expected in the form of a map. + void _handleInAppValidationResultListenerOnResponse( + Map>? callbackData) { + _handleValidationResultListenerOnResponse( + callbackData, + _viapOnResponse, + (value) => InAppPurchaseValidationResult.fromJson(value), + ); + } + + + /// Handles failure for the subscription purchase validation result listener. + /// + /// [callbackData] is the callback data expected in the form of a map. + void _handleSubscriptionPurchaseValidationResultListenerOnFailure( + Map callbackData) { + _handleValidationResultListenerOnFailure(callbackData, _arsOnFailure); + } + + /// Handles failure for the in-app validation result listener. + /// + /// [callbackData] is the callback data expected in the form of a map. + void _handleInAppValidationResultListenerOnFailure( + Map callbackData) { + _handleValidationResultListenerOnFailure(callbackData, _viapOnFailure); + } + + /// Handles the reception of purchase revenue validation info. + /// + /// [callbackData] is the callback data expected in the form of a map. + void _handleDidReceivePurchaseRevenueValidationInfo( + Map callbackData) { + var validationInfo = callbackData[AppsflyerConstants.VALIDATION_INFO] + as Map?; + var errorMap = + callbackData[AppsflyerConstants.ERROR] as Map?; + var error = errorMap != null ? IosError.fromJson(errorMap) : null; + if (_didReceivePurchaseRevenueValidationInfo != null) { + _didReceivePurchaseRevenueValidationInfo!(validationInfo, error); + } + } + + /// Handles the response for a validation result listener. + /// + /// [callbackData] is the callback data expected in the form of a map. + /// [onResponse] is a function to be called upon response. + /// [converter] is a function used for converting `[callbackData]` to result type `T` + void _handleValidationResultListenerOnResponse( + Map>? callbackData, + OnResponse? onResponse, + T Function(Map) converter) { + Map? res; + if (callbackData != null) { + res = { + for (var entry in callbackData.entries) + entry.key: converter(entry.value) + }; + } + if (onResponse != null) { + onResponse(res); + } + } + + /// Handles failure for a validation result listener. + /// + /// [callbackData] is the callback data expected in the form of a map. + /// [onFailureCallback] is a function to be called on failure. + void _handleValidationResultListenerOnFailure( + Map callbackData, OnFailure? onFailureCallback) { + var resultMsg = callbackData[AppsflyerConstants.RESULT] as String; + var errorMap = + callbackData[AppsflyerConstants.ERROR] as Map?; + var error = errorMap != null ? JVMThrowable.fromJson(errorMap) : null; + if (onFailureCallback != null) { + onFailureCallback(resultMsg, error); + } + } +} diff --git a/lib/src/purchase_connector/purchase_connector_configuration.dart b/lib/src/purchase_connector/purchase_connector_configuration.dart new file mode 100644 index 00000000..3832dc7d --- /dev/null +++ b/lib/src/purchase_connector/purchase_connector_configuration.dart @@ -0,0 +1,16 @@ +part of appsflyer_sdk; + +/// Contains the configuration settings for a `PurchaseConnector`. +/// +/// This class controls automatic logging of In-App purchase and subscription events. +/// It also allows setting a sandbox environment for validation. +class PurchaseConnectorConfiguration { + bool logSubscriptions; + bool logInApps; + bool sandbox; + + PurchaseConnectorConfiguration( + {this.logSubscriptions = false, + this.logInApps = false, + this.sandbox = false}); +} diff --git a/pubspec.yaml b/pubspec.yaml index fb17829f..6d75bf3f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,6 +11,7 @@ environment: dependencies: flutter: sdk: flutter + json_annotation: ^4.9.0 dev_dependencies: flutter_test: @@ -18,6 +19,8 @@ dev_dependencies: test: ^1.16.5 mockito: ^5.4.4 effective_dart: ^1.3.0 + build_runner: ^2.3.0 + json_serializable: ^6.5.4 flutter: From d92976e1710b85160f469348cd71baa2be600e7a Mon Sep 17 00:00:00 2001 From: Paz Lavi Date: Sun, 16 Jun 2024 13:55:16 +0300 Subject: [PATCH 02/42] runOnUi --- .../AppsFlyerPurchaseConnector.kt | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/android/src/main/include-connector/com/appsflyer/appsflyersdk/AppsFlyerPurchaseConnector.kt b/android/src/main/include-connector/com/appsflyer/appsflyersdk/AppsFlyerPurchaseConnector.kt index 0b57a576..27d0c487 100644 --- a/android/src/main/include-connector/com/appsflyer/appsflyersdk/AppsFlyerPurchaseConnector.kt +++ b/android/src/main/include-connector/com/appsflyer/appsflyersdk/AppsFlyerPurchaseConnector.kt @@ -1,10 +1,14 @@ package com.appsflyer.appsflyersdk import android.content.Context +import android.os.Handler +import android.os.Looper import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import java.lang.ref.WeakReference +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit /** * A Flutter plugin that establishes a bridge between the Flutter appsflyer SDK and the Native Android Purchase Connector. @@ -22,19 +26,20 @@ object AppsFlyerPurchaseConnector : FlutterPlugin, MethodChannel.MethodCallHandl private var methodChannel: MethodChannel? = null private var contextRef: WeakReference? = null private var connectorWrapper: ConnectorWrapper? = null + private val handler by lazy { Handler(Looper.getMainLooper()) } private val arsListener: MappedValidationResultListener by lazy { object : MappedValidationResultListener { override fun onFailure(result: String, error: Throwable?) { val resMap = mapOf("result" to result, "error" to error?.toMap()) - methodChannel?.invokeMethod( + methodChannel?.invokeMethodOnUI( "SubscriptionPurchaseValidationResultListener:onFailure", resMap ) } override fun onResponse(p0: Map?) { - methodChannel?.invokeMethod( + methodChannel?.invokeMethodOnUI( "SubscriptionPurchaseValidationResultListener:onResponse", p0 ) @@ -46,14 +51,20 @@ object AppsFlyerPurchaseConnector : FlutterPlugin, MethodChannel.MethodCallHandl object : MappedValidationResultListener { override fun onFailure(result: String, error: Throwable?) { val resMap = mapOf("result" to result, "error" to error?.toMap()) - methodChannel?.invokeMethod("InAppValidationResultListener:onFailure", resMap) + methodChannel?.invokeMethodOnUI("InAppValidationResultListener:onFailure", resMap) } override fun onResponse(p0: Map?) { - methodChannel?.invokeMethod("InAppValidationResultListener:onResponse", p0) + methodChannel?.invokeMethodOnUI("InAppValidationResultListener:onResponse", p0) } } } + private fun MethodChannel?.invokeMethodOnUI(method: String, args: Any) = this?.let { + handler.post { + it.invokeMethod(method, args) + } + } + /** * Called when the plugin is attached to the Flutter engine. @@ -160,6 +171,7 @@ object AppsFlyerPurchaseConnector : FlutterPlugin, MethodChannel.MethodCallHandl result.error("404", "Connector not configured, did you called `configure` first?", null) } } + /** * Converts a [Throwable] to a Map that can be returned to Flutter. * From 7b3f4e5b58da47c8939ca6bd5e65d1f9918882f3 Mon Sep 17 00:00:00 2001 From: Paz Lavi Date: Sun, 16 Jun 2024 14:10:04 +0300 Subject: [PATCH 03/42] Nullable --- .../com/appsflyer/appsflyersdk/AppsFlyerPurchaseConnector.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/src/main/include-connector/com/appsflyer/appsflyersdk/AppsFlyerPurchaseConnector.kt b/android/src/main/include-connector/com/appsflyer/appsflyersdk/AppsFlyerPurchaseConnector.kt index 27d0c487..99efb124 100644 --- a/android/src/main/include-connector/com/appsflyer/appsflyersdk/AppsFlyerPurchaseConnector.kt +++ b/android/src/main/include-connector/com/appsflyer/appsflyersdk/AppsFlyerPurchaseConnector.kt @@ -59,7 +59,7 @@ object AppsFlyerPurchaseConnector : FlutterPlugin, MethodChannel.MethodCallHandl } } } - private fun MethodChannel?.invokeMethodOnUI(method: String, args: Any) = this?.let { + private fun MethodChannel?.invokeMethodOnUI(method: String, args: Any?) = this?.let { handler.post { it.invokeMethod(method, args) } From cc4dc4aa901ae4c17c2f18a9ca3c39716a93adc4 Mon Sep 17 00:00:00 2001 From: Paz Lavi Date: Sun, 16 Jun 2024 16:37:08 +0300 Subject: [PATCH 04/42] handle callbacks --- .../AppsFlyerPurchaseConnector.kt | 12 +++- .../PurchaseConnectorPlugin.swift | 17 +++++- lib/appsflyer_sdk.g.dart | 30 ++++++++++ .../in_app_purchase_validation_result.dart | 13 +++- .../subscription_validation_result.dart | 12 +++- .../purchase_connector.dart | 60 +++++++------------ 6 files changed, 101 insertions(+), 43 deletions(-) diff --git a/android/src/main/include-connector/com/appsflyer/appsflyersdk/AppsFlyerPurchaseConnector.kt b/android/src/main/include-connector/com/appsflyer/appsflyersdk/AppsFlyerPurchaseConnector.kt index 99efb124..a923fe9e 100644 --- a/android/src/main/include-connector/com/appsflyer/appsflyersdk/AppsFlyerPurchaseConnector.kt +++ b/android/src/main/include-connector/com/appsflyer/appsflyersdk/AppsFlyerPurchaseConnector.kt @@ -6,9 +6,9 @@ import android.os.Looper import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel +import org.json.JSONObject import java.lang.ref.WeakReference -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit + /** * A Flutter plugin that establishes a bridge between the Flutter appsflyer SDK and the Native Android Purchase Connector. @@ -59,9 +59,15 @@ object AppsFlyerPurchaseConnector : FlutterPlugin, MethodChannel.MethodCallHandl } } } + private fun MethodChannel?.invokeMethodOnUI(method: String, args: Any?) = this?.let { handler.post { - it.invokeMethod(method, args) + val data = if (args is Map<*, *>) { + JSONObject(args).toString() + } else { + args + } + it.invokeMethod(method, data) } } diff --git a/ios/PurchaseConnector/PurchaseConnectorPlugin.swift b/ios/PurchaseConnector/PurchaseConnectorPlugin.swift index 559f683e..29834054 100644 --- a/ios/PurchaseConnector/PurchaseConnectorPlugin.swift +++ b/ios/PurchaseConnector/PurchaseConnectorPlugin.swift @@ -135,7 +135,7 @@ extension PurchaseConnectorPlugin: PurchaseRevenueDelegate { "validationInfo": validationInfo, "error" : error?.asDictionary ] - methodChannel?.invokeMethod("didReceivePurchaseRevenueValidationInfo", arguments: resMap) + methodChannel?.invokeMethod("didReceivePurchaseRevenueValidationInfo", arguments: resMap.toJSONString()) } } @@ -150,3 +150,18 @@ extension Error { return errorMap } } + +extension Dictionary { + + var jsonData: Data? { + return try? JSONSerialization.data(withJSONObject: self, options: [.prettyPrinted]) + } + + func toJSONString() -> String? { + if let jsonData = jsonData { + let jsonString = String(data: jsonData, encoding: .utf8) + return jsonString + } + return nil + } +} diff --git a/lib/appsflyer_sdk.g.dart b/lib/appsflyer_sdk.g.dart index 64ce3e8a..65275103 100644 --- a/lib/appsflyer_sdk.g.dart +++ b/lib/appsflyer_sdk.g.dart @@ -329,6 +329,21 @@ Map _$InAppPurchaseValidationResultToJson( 'failureData': instance.failureData, }; +InAppPurchaseValidationResultMap _$InAppPurchaseValidationResultMapFromJson( + Map json) => + InAppPurchaseValidationResultMap( + (json['result'] as Map).map( + (k, e) => MapEntry(k, + InAppPurchaseValidationResult.fromJson(e as Map)), + ), + ); + +Map _$InAppPurchaseValidationResultMapToJson( + InAppPurchaseValidationResultMap instance) => + { + 'result': instance.result, + }; + ProductPurchase _$ProductPurchaseFromJson(Map json) => ProductPurchase( json['kind'] as String, @@ -387,6 +402,21 @@ Map _$SubscriptionValidationResultToJson( 'failureData': instance.failureData, }; +SubscriptionValidationResultMap _$SubscriptionValidationResultMapFromJson( + Map json) => + SubscriptionValidationResultMap( + (json['result'] as Map).map( + (k, e) => MapEntry(k, + SubscriptionValidationResult.fromJson(e as Map)), + ), + ); + +Map _$SubscriptionValidationResultMapToJson( + SubscriptionValidationResultMap instance) => + { + 'result': instance.result, + }; + ValidationFailureData _$ValidationFailureDataFromJson( Map json) => ValidationFailureData( diff --git a/lib/src/purchase_connector/models/in_app_purchase_validation_result.dart b/lib/src/purchase_connector/models/in_app_purchase_validation_result.dart index 6f8f909f..0846a180 100644 --- a/lib/src/purchase_connector/models/in_app_purchase_validation_result.dart +++ b/lib/src/purchase_connector/models/in_app_purchase_validation_result.dart @@ -19,4 +19,15 @@ class InAppPurchaseValidationResult { Map toJson() => _$InAppPurchaseValidationResultToJson(this); -} \ No newline at end of file +} + +@JsonSerializable() +class InAppPurchaseValidationResultMap{ + Map result; + + InAppPurchaseValidationResultMap(this.result); + factory InAppPurchaseValidationResultMap.fromJson(Map json) => _$InAppPurchaseValidationResultMapFromJson(json); + + Map toJson() => _$InAppPurchaseValidationResultMapToJson(this); + +} diff --git a/lib/src/purchase_connector/models/subscription_validation_result.dart b/lib/src/purchase_connector/models/subscription_validation_result.dart index a94647bd..b9294176 100644 --- a/lib/src/purchase_connector/models/subscription_validation_result.dart +++ b/lib/src/purchase_connector/models/subscription_validation_result.dart @@ -19,4 +19,14 @@ class SubscriptionValidationResult { Map toJson() => _$SubscriptionValidationResultToJson(this); -} \ No newline at end of file +} + +@JsonSerializable() +class SubscriptionValidationResultMap{ + Map result; + + SubscriptionValidationResultMap(this.result); + factory SubscriptionValidationResultMap.fromJson(Map json) => _$SubscriptionValidationResultMapFromJson(json); + + Map toJson() => _$SubscriptionValidationResultMapToJson(this); +} diff --git a/lib/src/purchase_connector/purchase_connector.dart b/lib/src/purchase_connector/purchase_connector.dart index 4f5acc8c..e962823e 100644 --- a/lib/src/purchase_connector/purchase_connector.dart +++ b/lib/src/purchase_connector/purchase_connector.dart @@ -1,9 +1,7 @@ part of appsflyer_sdk; - /// Interface representing a purchase connector. abstract class PurchaseConnector { - /// Starts observing transactions. void startObservingTransactions(); @@ -40,13 +38,11 @@ abstract class PurchaseConnector { _PurchaseConnectorImpl(config: config); } - /// The implementation of the PurchaseConnector. /// /// This class is responsible for establishing a connection with Appsflyer purchase connector, /// starting/stopping observing transactions, setting listeners for various validation results. class _PurchaseConnectorImpl implements PurchaseConnector { - /// Singleton instance of the PurchaseConnectorImpl. static _PurchaseConnectorImpl? _instance; @@ -55,12 +51,16 @@ class _PurchaseConnectorImpl implements PurchaseConnector { /// Response handler for SubscriptionValidationResult (Android). OnResponse? _arsOnResponse; + /// Failure handler for SubscriptionValidationResult (Android). OnFailure? _arsOnFailure; + /// Response handler for InAppPurchaseValidationResult (Android). OnResponse? _viapOnResponse; + /// Failure handler for InAppPurchaseValidationResult (Android). OnFailure? _viapOnFailure; + /// Callback handler for receiving validation info for iOS. DidReceivePurchaseRevenueValidationInfo? _didReceivePurchaseRevenueValidationInfo; @@ -146,28 +146,24 @@ class _PurchaseConnectorImpl implements PurchaseConnector { /// Method call handler for different operations. Called by the _methodChannel. Future _methodCallHandler(MethodCall call) async { + dynamic callMap = jsonDecode(call.arguments); switch (call.method) { case AppsflyerConstants .SUBSCRIPTION_PURCHASE_VALIDATION_RESULT_LISTENER_ON_RESPONSE: - _handleSubscriptionPurchaseValidationResultListenerOnResponse( - call.arguments as Map>?); + _handleSubscriptionPurchaseValidationResultListenerOnResponse(callMap); break; case AppsflyerConstants .SUBSCRIPTION_PURCHASE_VALIDATION_RESULT_LISTENER_ON_FAILURE: - _handleSubscriptionPurchaseValidationResultListenerOnFailure( - call.arguments as Map); + _handleSubscriptionPurchaseValidationResultListenerOnFailure(callMap); break; case AppsflyerConstants.IN_APP_VALIDATION_RESULT_LISTENER_ON_RESPONSE: - _handleInAppValidationResultListenerOnResponse( - call.arguments as Map>?); + _handleInAppValidationResultListenerOnResponse(callMap); break; case AppsflyerConstants.IN_APP_VALIDATION_RESULT_LISTENER_ON_FAILURE: - _handleInAppValidationResultListenerOnFailure( - call.arguments as Map); + _handleInAppValidationResultListenerOnFailure(callMap); break; case AppsflyerConstants.DID_RECEIVE_PURCHASE_REVENUE_VALIDATION_INFO: - _handleDidReceivePurchaseRevenueValidationInfo( - call.arguments as Map); + _handleDidReceivePurchaseRevenueValidationInfo(callMap); break; default: throw ArgumentError("Method not found."); @@ -178,11 +174,11 @@ class _PurchaseConnectorImpl implements PurchaseConnector { /// /// [callbackData] is the callback data expected in the form of a map. void _handleSubscriptionPurchaseValidationResultListenerOnResponse( - Map>? callbackData) { + dynamic callbackData) { _handleValidationResultListenerOnResponse( - callbackData, + {"result": callbackData}, _arsOnResponse, - (value) => SubscriptionValidationResult.fromJson(value), + (value) => SubscriptionValidationResultMap.fromJson(value).result, ); } @@ -190,15 +186,14 @@ class _PurchaseConnectorImpl implements PurchaseConnector { /// /// [callbackData] is the callback data expected in the form of a map. void _handleInAppValidationResultListenerOnResponse( - Map>? callbackData) { + dynamic callbackData) { _handleValidationResultListenerOnResponse( - callbackData, + {"result": callbackData}, _viapOnResponse, - (value) => InAppPurchaseValidationResult.fromJson(value), + (value) => InAppPurchaseValidationResultMap.fromJson(value).result, ); } - /// Handles failure for the subscription purchase validation result listener. /// /// [callbackData] is the callback data expected in the form of a map. @@ -210,16 +205,14 @@ class _PurchaseConnectorImpl implements PurchaseConnector { /// Handles failure for the in-app validation result listener. /// /// [callbackData] is the callback data expected in the form of a map. - void _handleInAppValidationResultListenerOnFailure( - Map callbackData) { + void _handleInAppValidationResultListenerOnFailure(dynamic callbackData) { _handleValidationResultListenerOnFailure(callbackData, _viapOnFailure); } /// Handles the reception of purchase revenue validation info. /// /// [callbackData] is the callback data expected in the form of a map. - void _handleDidReceivePurchaseRevenueValidationInfo( - Map callbackData) { + void _handleDidReceivePurchaseRevenueValidationInfo(dynamic callbackData) { var validationInfo = callbackData[AppsflyerConstants.VALIDATION_INFO] as Map?; var errorMap = @@ -235,19 +228,12 @@ class _PurchaseConnectorImpl implements PurchaseConnector { /// [callbackData] is the callback data expected in the form of a map. /// [onResponse] is a function to be called upon response. /// [converter] is a function used for converting `[callbackData]` to result type `T` - void _handleValidationResultListenerOnResponse( - Map>? callbackData, - OnResponse? onResponse, - T Function(Map) converter) { - Map? res; - if (callbackData != null) { - res = { - for (var entry in callbackData.entries) - entry.key: converter(entry.value) - }; - } + void _handleValidationResultListenerOnResponse(dynamic callbackData, + OnResponse? onResponse, Map? Function(dynamic) converter) { + Map? res = converter(callbackData); if (onResponse != null) { onResponse(res); + } else { } } @@ -256,7 +242,7 @@ class _PurchaseConnectorImpl implements PurchaseConnector { /// [callbackData] is the callback data expected in the form of a map. /// [onFailureCallback] is a function to be called on failure. void _handleValidationResultListenerOnFailure( - Map callbackData, OnFailure? onFailureCallback) { + dynamic callbackData, OnFailure? onFailureCallback) { var resultMsg = callbackData[AppsflyerConstants.RESULT] as String; var errorMap = callbackData[AppsflyerConstants.ERROR] as Map?; From f2012991c4bc3379f673305de5c4ff8395c6c40a Mon Sep 17 00:00:00 2001 From: Paz Lavi Date: Sun, 16 Jun 2024 17:19:16 +0300 Subject: [PATCH 05/42] ui thread callback --- ios/PurchaseConnector/PurchaseConnectorPlugin.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ios/PurchaseConnector/PurchaseConnectorPlugin.swift b/ios/PurchaseConnector/PurchaseConnectorPlugin.swift index 29834054..f3a3927c 100644 --- a/ios/PurchaseConnector/PurchaseConnectorPlugin.swift +++ b/ios/PurchaseConnector/PurchaseConnectorPlugin.swift @@ -135,7 +135,9 @@ extension PurchaseConnectorPlugin: PurchaseRevenueDelegate { "validationInfo": validationInfo, "error" : error?.asDictionary ] - methodChannel?.invokeMethod("didReceivePurchaseRevenueValidationInfo", arguments: resMap.toJSONString()) + DispatchQueue.main.async { + self.methodChannel?.invokeMethod("didReceivePurchaseRevenueValidationInfo", arguments: resMap.toJSONString()) + } } } From 03db5e47ff80e11eca207873ba74618a141b3c1c Mon Sep 17 00:00:00 2001 From: Paz Lavi Date: Sun, 16 Jun 2024 20:08:08 +0300 Subject: [PATCH 06/42] swift bridge file --- ios/Classes/AppsflyerSdkPlugin.m | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ios/Classes/AppsflyerSdkPlugin.m b/ios/Classes/AppsflyerSdkPlugin.m index c7be3a11..234ad807 100644 --- a/ios/Classes/AppsflyerSdkPlugin.m +++ b/ios/Classes/AppsflyerSdkPlugin.m @@ -1,9 +1,10 @@ #import "AppsflyerSdkPlugin.h" #import "AppsFlyerStreamHandler.h" #import -#import "appsflyer_sdk/appsflyer_sdk-Swift.h" - +#ifdef ENABLE_PURCHASE_CONNECTOR +#import "appsflyer_sdk/appsflyer_sdk-Swift.h" +#endif typedef void (*bypassDidFinishLaunchingWithOption)(id, SEL, NSInteger); typedef void (*bypassDisableAdvertisingIdentifier)(id, SEL, BOOL); typedef void (*bypassWaitForATTUserAuthorization)(id, SEL, NSTimeInterval); From 8729a3d22efb232563022ecd70a7731b45716462 Mon Sep 17 00:00:00 2001 From: Paz Lavi Date: Sun, 16 Jun 2024 23:18:50 +0300 Subject: [PATCH 07/42] register callback --- ios/PurchaseConnector/PurchaseConnectorPlugin.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/ios/PurchaseConnector/PurchaseConnectorPlugin.swift b/ios/PurchaseConnector/PurchaseConnectorPlugin.swift index f3a3927c..dfd60f41 100644 --- a/ios/PurchaseConnector/PurchaseConnectorPlugin.swift +++ b/ios/PurchaseConnector/PurchaseConnectorPlugin.swift @@ -91,6 +91,7 @@ import Flutter connector!.autoLogPurchaseRevenue = options logOptions = options connector!.isSandbox = sandbox + connector!.purchaseRevenueDelegate = self /// Report a successful operation back to Dart. result(nil) From 7d78d0c88ad764e7c0f14337912e936979b87038 Mon Sep 17 00:00:00 2001 From: Paz Lavi Date: Mon, 17 Jun 2024 11:51:43 +0300 Subject: [PATCH 08/42] docs --- doc/PurchaseConnector.md | 45 ++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/doc/PurchaseConnector.md b/doc/PurchaseConnector.md index db266895..b72c61b6 100644 --- a/doc/PurchaseConnector.md +++ b/doc/PurchaseConnector.md @@ -13,28 +13,29 @@ support@appsflyer.com ## Table Of Content -* [ âš ī¸ âš ī¸ Important Note âš ī¸ âš ī¸ ](#-important-note-) -* [ Adding The Connector To Your Project](#adding-the-connector-to-your-project) - + [How to Opt-In](#how-to-opt-in) - + [What Happens if You Use Dart Files Without Opting In?](#what-happens-if-you-use-dart-files-without-opting-in) -* [ Basic Integration Of The Connector](#basic-integration-of-the-connector) - + [ Create PurchaseConnector Instance](#create-purchaseconnector-instance) - + [ Start Observing Transactions](#start-observing-transactions) - + [ Stop Observing Transactions](#stop-observing-transactions) - + [ Log Subscriptions](#log-subscriptions) - + [ Log In App Purchases](#log-in-app-purchases) -* [ Register Validation Results Listeners](#register-validation-results-listeners) - + [ Cross-Platform Considerations](#cross-platform-considerations) - + [ Android Callback Types](#android-callback-types) - + [ Android - Subscription Validation Result Listener ](#android-subscription-validation-result-listener) - + [ Android In Apps Validation Result Listener](#android-in-apps-validation-result-listener) - + [ iOS Combined Validation Result Listener](#ios-combined-validation-result-listener) -* [ Testing the Integration](#testing-the-integration) - + [ Android](#android) - + [ iOS](#ios) - + [ Dart Usage for Android and iOS](#dart-usage-for-android-and-ios) -* [ ProGuard Rules for Android](#proguard-rules-for-android) -* [ Full Code Example](#full-code-example) + +* [Important Note](#important-note) +* [Adding The Connector To Your Project](#install-connector) + - [How to Opt-In](#install-connector) + - [What Happens if You Use Dart Files Without Opting In?](#install-connector) +* [Basic Integration Of The Connector](#basic-integration) + - [Create PurchaseConnector Instance](#create-instance) + - [Start Observing Transactions](#start) + - [Stop Observing Transactions](#stop) + - [Log Subscriptions](#log-subscriptions) + - [Log In App Purchases](#log-inapps) +* [Register Validation Results Listeners](#validation-callbacks) + - [Cross-Platform Considerations](#cross-platform-considerations) + - [Android Callback Types](#android-callback-types) + - [Android - Subscription Validation Result Listener](#ars-validation-callbacks) + - [Android In Apps Validation Result Listener](#inapps-validation-callbacks) + - [iOS Combined Validation Result Listener](#ios-callback) +* [Testing the Integration](#testing) + - [Android](#testing-android) + - [iOS](#testing-ios) + - [Dart Usage for Android and iOS](#testing-config) +* [ProGuard Rules for Android](#proguard) +* [Full Code Example](#example) From 6529458e88ea70e2d52bdd981b37640906c414a4 Mon Sep 17 00:00:00 2001 From: Dani-Koza-AF Date: Sun, 27 Oct 2024 17:47:00 +0200 Subject: [PATCH 09/42] fixed the issue with MediationNetwork enums on Android - added a usage example of the api for testing. --- example/lib/home_container.dart | 16 ++++++++++++++++ example/lib/main_page.dart | 32 +++++++++++++++++++++++++------- ios/Classes/AppsflyerSdkPlugin.m | 20 ++++++++++---------- lib/src/appsflyer_constants.dart | 20 ++++++++++---------- 4 files changed, 61 insertions(+), 27 deletions(-) diff --git a/example/lib/home_container.dart b/example/lib/home_container.dart index 43d8d82a..a4748309 100644 --- a/example/lib/home_container.dart +++ b/example/lib/home_container.dart @@ -7,12 +7,14 @@ import 'utils.dart'; class HomeContainer extends StatefulWidget { final Map onData; final Future Function(String, Map) logEvent; + final void Function() logAdRevenueEvent; Object deepLinkData; HomeContainer({ required this.onData, required this.deepLinkData, required this.logEvent, + required this.logAdRevenueEvent, }); @override @@ -136,6 +138,20 @@ class _HomeContainerState extends State { ), ), ), + ElevatedButton( + onPressed: () { + widget.logAdRevenueEvent(); + }, + child: Text("Trigger AdRevenue Event"), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10), + textStyle: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ), ], ), ) diff --git a/example/lib/main_page.dart b/example/lib/main_page.dart index e0da017e..70ce33b7 100644 --- a/example/lib/main_page.dart +++ b/example/lib/main_page.dart @@ -124,6 +124,7 @@ class MainPageState extends State { onData: _gcd, deepLinkData: _deepLinkData, logEvent: logEvent, + logAdRevenueEvent: logAdRevenueEvent, ), ), ElevatedButton( @@ -133,7 +134,8 @@ class MainPageState extends State { showMessage("AppsFlyer SDK initialized successfully."); }, onError: (int errorCode, String errorMessage) { - showMessage("Error initializing AppsFlyer SDK: Code $errorCode - $errorMessage"); + showMessage( + "Error initializing AppsFlyer SDK: Code $errorCode - $errorMessage"); }, ); }, @@ -158,13 +160,29 @@ class MainPageState extends State { return logResult; } + void logAdRevenueEvent() { + try { + Map customParams = { + 'ad_platform': 'Admob', + 'ad_currency': 'USD', + }; + + AdRevenueData adRevenueData = AdRevenueData( + monetizationNetwork: 'SpongeBob', + mediationNetwork: AFMediationNetwork.googleAdMob.value, + currencyIso4217Code: 'USD', + revenue: 100.3, + additionalParameters: customParams); + _appsflyerSdk.logAdRevenue(adRevenueData); + print("Ad Revenue event logged with no errors"); + } catch (e) { + print("Failed to log event: $e"); + } + } + void showMessage(String message) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar( - content: - Text(message), + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(message), )); } } - - diff --git a/ios/Classes/AppsflyerSdkPlugin.m b/ios/Classes/AppsflyerSdkPlugin.m index 4295044a..f497c68a 100644 --- a/ios/Classes/AppsflyerSdkPlugin.m +++ b/ios/Classes/AppsflyerSdkPlugin.m @@ -255,20 +255,20 @@ - (void)logAdRevenue:(FlutterMethodCall*)call result:(FlutterResult)result { - (AppsFlyerAdRevenueMediationNetworkType)getEnumValueFromString:(NSString *)mediationNetworkString { NSDictionary *stringToEnumMap = @{ - @"googleadmob": @(AppsFlyerAdRevenueMediationNetworkTypeGoogleAdMob), + @"google_admob": @(AppsFlyerAdRevenueMediationNetworkTypeGoogleAdMob), @"ironsource": @(AppsFlyerAdRevenueMediationNetworkTypeIronSource), - @"applovinmax": @(AppsFlyerAdRevenueMediationNetworkTypeApplovinMax), + @"applovin_max": @(AppsFlyerAdRevenueMediationNetworkTypeApplovinMax), @"fyber": @(AppsFlyerAdRevenueMediationNetworkTypeFyber), @"appodeal": @(AppsFlyerAdRevenueMediationNetworkTypeAppodeal), - @"Admost": @(AppsFlyerAdRevenueMediationNetworkTypeAdmost), - @"Topon": @(AppsFlyerAdRevenueMediationNetworkTypeTopon), - @"Tradplus": @(AppsFlyerAdRevenueMediationNetworkTypeTradplus), - @"Yandex": @(AppsFlyerAdRevenueMediationNetworkTypeYandex), + @"admost": @(AppsFlyerAdRevenueMediationNetworkTypeAdmost), + @"topon": @(AppsFlyerAdRevenueMediationNetworkTypeTopon), + @"tradplus": @(AppsFlyerAdRevenueMediationNetworkTypeTradplus), + @"yandex": @(AppsFlyerAdRevenueMediationNetworkTypeYandex), @"chartboost": @(AppsFlyerAdRevenueMediationNetworkTypeChartBoost), - @"Unity": @(AppsFlyerAdRevenueMediationNetworkTypeUnity), - @"toponpte": @(AppsFlyerAdRevenueMediationNetworkTypeToponPte), - @"customMediation": @(AppsFlyerAdRevenueMediationNetworkTypeCustom), - @"directMonetizationNetwork": @(AppsFlyerAdRevenueMediationNetworkTypeDirectMonetization) + @"unity": @(AppsFlyerAdRevenueMediationNetworkTypeUnity), + @"topon_pte": @(AppsFlyerAdRevenueMediationNetworkTypeToponPte), + @"custom_mediation": @(AppsFlyerAdRevenueMediationNetworkTypeCustom), + @"direct_monetization_network": @(AppsFlyerAdRevenueMediationNetworkTypeDirectMonetization) }; NSNumber *enumValueNumber = stringToEnumMap[mediationNetworkString]; diff --git a/lib/src/appsflyer_constants.dart b/lib/src/appsflyer_constants.dart index 6f86f562..641db02e 100644 --- a/lib/src/appsflyer_constants.dart +++ b/lib/src/appsflyer_constants.dart @@ -49,31 +49,31 @@ enum AFMediationNetwork { case AFMediationNetwork.ironSource: return "ironsource"; case AFMediationNetwork.applovinMax: - return "applovinmax"; + return "applovin_max"; case AFMediationNetwork.googleAdMob: - return "googleadmob"; + return "google_admob"; case AFMediationNetwork.fyber: return "fyber"; case AFMediationNetwork.appodeal: return "appodeal"; case AFMediationNetwork.admost: - return "Admost"; + return "admost"; case AFMediationNetwork.topon: - return "Topon"; + return "topon"; case AFMediationNetwork.tradplus: - return "Tradplus"; + return "tradplus"; case AFMediationNetwork.yandex: - return "Yandex"; + return "yandex"; case AFMediationNetwork.chartboost: return "chartboost"; case AFMediationNetwork.unity: - return "Unity"; + return "unity"; case AFMediationNetwork.toponPte: - return "toponpte"; + return "topon_pte"; case AFMediationNetwork.customMediation: - return "customMediation"; + return "custom_mediation"; case AFMediationNetwork.directMonetizationNetwork: - return "directMonetizationNetwork"; + return "direct_monetization_network"; } } } From 25fb530b85c2dd46e5fdcbac85784067f89514f4 Mon Sep 17 00:00:00 2001 From: Dani-Koza-AF Date: Wed, 30 Oct 2024 14:26:26 +0200 Subject: [PATCH 10/42] This should fix the NullPointerException --- .../appsflyersdk/AppsFlyerConstants.java | 52 +++++++++-------- .../appsflyersdk/AppsflyerSdkPlugin.java | 57 ++++++++++--------- .../appsflyer/appsflyersdk/LogMessages.java | 12 ++++ 3 files changed, 69 insertions(+), 52 deletions(-) create mode 100644 android/src/main/java/com/appsflyer/appsflyersdk/LogMessages.java diff --git a/android/src/main/java/com/appsflyer/appsflyersdk/AppsFlyerConstants.java b/android/src/main/java/com/appsflyer/appsflyersdk/AppsFlyerConstants.java index b6f419c7..c8a7847a 100644 --- a/android/src/main/java/com/appsflyer/appsflyersdk/AppsFlyerConstants.java +++ b/android/src/main/java/com/appsflyer/appsflyersdk/AppsFlyerConstants.java @@ -1,30 +1,32 @@ package com.appsflyer.appsflyersdk; -public class AppsFlyerConstants { - final static String PLUGIN_VERSION = "6.15.1"; - final static String AF_APP_INVITE_ONE_LINK = "appInviteOneLink"; - final static String AF_HOST_PREFIX = "hostPrefix"; - final static String AF_HOST_NAME = "hostName"; - final static String AF_IS_DEBUG = "isDebug"; - final static String AF_MANUAL_START = "manualStart"; - final static String AF_DEV_KEY = "afDevKey"; - final static String AF_EVENT_NAME = "eventName"; - final static String AF_EVENT_VALUES = "eventValues"; - final static String AF_ON_INSTALL_CONVERSION_DATA_LOADED = "onInstallConversionDataLoaded"; - final static String AF_ON_APP_OPEN_ATTRIBUTION = "onAppOpenAttribution"; - final static String AF_SUCCESS = "success"; - final static String AF_FAILURE = "failure"; - final static String AF_GCD = "GCD"; - final static String AF_UDL = "UDL"; - final static String AF_VALIDATE_PURCHASE = "validatePurchase"; - final static String AF_GCD_CALLBACK = "onInstallConversionData"; - final static String AF_OAOA_CALLBACK = "onAppOpenAttribution"; - final static String AF_UDL_CALLBACK = "onDeepLinking"; - final static String DISABLE_ADVERTISING_IDENTIFIER = "disableAdvertisingIdentifier"; +public final class AppsFlyerConstants { + final static String PLUGIN_VERSION = "6.15.1"; + final static String AF_APP_INVITE_ONE_LINK = "appInviteOneLink"; + final static String AF_HOST_PREFIX = "hostPrefix"; + final static String AF_HOST_NAME = "hostName"; + final static String AF_IS_DEBUG = "isDebug"; + final static String AF_MANUAL_START = "manualStart"; + final static String AF_DEV_KEY = "afDevKey"; + final static String AF_EVENT_NAME = "eventName"; + final static String AF_EVENT_VALUES = "eventValues"; + final static String AF_ON_INSTALL_CONVERSION_DATA_LOADED = "onInstallConversionDataLoaded"; + final static String AF_ON_APP_OPEN_ATTRIBUTION = "onAppOpenAttribution"; + final static String AF_SUCCESS = "success"; + final static String AF_FAILURE = "failure"; + final static String AF_GCD = "GCD"; + final static String AF_UDL = "UDL"; + final static String AF_VALIDATE_PURCHASE = "validatePurchase"; + final static String AF_GCD_CALLBACK = "onInstallConversionData"; + final static String AF_OAOA_CALLBACK = "onAppOpenAttribution"; + final static String AF_UDL_CALLBACK = "onDeepLinking"; + final static String DISABLE_ADVERTISING_IDENTIFIER = "disableAdvertisingIdentifier"; - final static String AF_EVENTS_CHANNEL = "af-events"; - final static String AF_METHOD_CHANNEL = "af-api"; - final static String AF_CALLBACK_CHANNEL = "callbacks"; + final static String AF_EVENTS_CHANNEL = "af-events"; + final static String AF_METHOD_CHANNEL = "af-api"; + final static String AF_CALLBACK_CHANNEL = "callbacks"; - final static String AF_BROADCAST_ACTION_NAME = "com.appsflyer.appsflyersdk"; + final static String AF_BROADCAST_ACTION_NAME = "com.appsflyer.appsflyersdk"; + + final static String AF_PLUGIN_TAG = "AppsFlyer_FlutterPlugin"; } diff --git a/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java b/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java index bc66b4ac..be1d7e63 100644 --- a/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java +++ b/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java @@ -49,6 +49,7 @@ import static com.appsflyer.appsflyersdk.AppsFlyerConstants.AF_EVENTS_CHANNEL; import static com.appsflyer.appsflyersdk.AppsFlyerConstants.AF_FAILURE; +import static com.appsflyer.appsflyersdk.AppsFlyerConstants.AF_PLUGIN_TAG; import static com.appsflyer.appsflyersdk.AppsFlyerConstants.AF_SUCCESS; /** @@ -203,7 +204,7 @@ private void startListening(Object arguments, Result rawResult) { @Override public void onMethodCall(MethodCall call, Result result) { if (activity == null) { - Log.d("AppsFlyer", "Activity isn't attached to the flutter engine"); + Log.d(AF_PLUGIN_TAG, LogMessages.ACTIVITY_NOT_ATTACHED_TO_ENGINE); return; } final String method = call.method; @@ -362,11 +363,11 @@ private void performOnDeepLinking(MethodCall call, Result result) { AppsFlyerLib.getInstance().performOnDeepLinking(intent, mApplication); result.success(null); } else { - Log.d("AppsFlyer", "performOnDeepLinking: intent is null!"); + Log.d(AF_PLUGIN_TAG, "performOnDeepLinking: intent is null!"); result.error("NO_INTENT", "The intent is null", null); } } else { - Log.d("AppsFlyer", "performOnDeepLinking: activity is null!"); + Log.d(AF_PLUGIN_TAG, "performOnDeepLinking: activity is null!"); result.error("NO_ACTIVITY", "The current activity is null", null); } } @@ -379,34 +380,37 @@ private void anonymizeUser(MethodCall call, Result result) { private void startSDKwithHandler(MethodCall call, final Result result) { try { - final AppsFlyerLib instance = AppsFlyerLib.getInstance(); - instance.start(activity, null, new AppsFlyerRequestListener() { + final AppsFlyerLib appsFlyerLib = AppsFlyerLib.getInstance(); + + appsFlyerLib.start(activity, null, new AppsFlyerRequestListener() { @Override public void onSuccess() { - uiThreadHandler.post(new Runnable() { - @Override - public void run() { - mMethodChannel.invokeMethod("onSuccess", null); - } - }); + if (mMethodChannel != null) { + uiThreadHandler.post(() -> mMethodChannel.invokeMethod("onSuccess", null)); + } else { + Log.e(AF_PLUGIN_TAG, LogMessages.METHOD_CHANNEL_IS_NULL); + result.error("NULL_OBJECT", LogMessages.METHOD_CHANNEL_IS_NULL, null); + } } @Override public void onError(final int errorCode, final String errorMessage) { - uiThreadHandler.post(new Runnable() { - @Override - public void run() { + if (mMethodChannel != null) { + uiThreadHandler.post(() -> { HashMap errorDetails = new HashMap<>(); errorDetails.put("errorCode", errorCode); errorDetails.put("errorMessage", errorMessage); mMethodChannel.invokeMethod("onError", errorDetails); - } - }); + }); + } else { + Log.e(AF_PLUGIN_TAG, LogMessages.METHOD_CHANNEL_IS_NULL); + result.error("NULL_OBJECT", LogMessages.METHOD_CHANNEL_IS_NULL, null); + } } }); result.success(null); - } catch (Exception e) { - result.error("UNEXPECTED_ERROR", e.getMessage(), null); + } catch (Throwable t) { + result.error("UNEXPECTED_ERROR", t.getMessage(), null); } } @@ -532,14 +536,14 @@ private void sendPushNotificationData(MethodCall call, Result result) { Bundle bundle; if (pushPayload == null) { - Log.d("AppsFlyer", "Push payload is null"); + Log.d(AF_PLUGIN_TAG, "Push payload is null"); return; } try { bundle = this.jsonToBundle(new JSONObject(pushPayload)); } catch (JSONException e) { - Log.d("AppsFlyer", "Can't parse pushPayload to bundle"); + Log.d(AF_PLUGIN_TAG, "Can't parse pushPayload to bundle"); return; } @@ -557,7 +561,7 @@ private void sendPushNotificationData(MethodCall call, Result result) { } if (errorMsg != null) { - Log.d("AppsFlyer", errorMsg); + Log.d(AF_PLUGIN_TAG, errorMsg); return; } @@ -963,8 +967,8 @@ private void logAdRevenue(MethodCall call, Result result) { try { String monetizationNetwork = requireNonNullArgument(call, "monetizationNetwork"); String currencyIso4217Code = requireNonNullArgument(call, "currencyIso4217Code"); - double revenue = requireNonNullArgument(call,"revenue"); - String mediationNetworkString = requireNonNullArgument(call,"mediationNetwork"); + double revenue = requireNonNullArgument(call, "revenue"); + String mediationNetworkString = requireNonNullArgument(call, "mediationNetwork"); MediationNetwork mediationNetwork = MediationNetwork.valueOf(mediationNetworkString.toUpperCase()); @@ -984,10 +988,9 @@ private void logAdRevenue(MethodCall call, Result result) { } catch (IllegalArgumentException e) { // The IllegalArgumentException could come from either requireNonNullArgument or valueOf methods. result.error("INVALID_ARGUMENT_PROVIDED", e.getMessage(), null); - } - catch (Throwable t) { + } catch (Throwable t) { result.error("UNEXPECTED_ERROR", "[logAdRevenue]: An unexpected error occurred: " + t.getMessage(), null); - Log.e("AppsFlyer", "Unexpected exception occurred: [logAdRevenue]", t); + Log.e(AF_PLUGIN_TAG, "Unexpected exception occurred: [logAdRevenue]", t); } } @@ -1004,7 +1007,7 @@ private void logAdRevenue(MethodCall call, Result result) { private T requireNonNullArgument(MethodCall call, String argumentName) throws IllegalArgumentException { T argument = call.argument(argumentName); if (argument == null) { - Log.e("AppsFlyer", "Exception occurred when trying to: " + call.method + "->" + argumentName + " must not be null"); + Log.e(AF_PLUGIN_TAG, "Exception occurred when trying to: " + call.method + "->" + argumentName + " must not be null"); throw new IllegalArgumentException("[" + call.method + "]: " + argumentName + " must not be null"); } return argument; diff --git a/android/src/main/java/com/appsflyer/appsflyersdk/LogMessages.java b/android/src/main/java/com/appsflyer/appsflyersdk/LogMessages.java new file mode 100644 index 00000000..cc363ef3 --- /dev/null +++ b/android/src/main/java/com/appsflyer/appsflyersdk/LogMessages.java @@ -0,0 +1,12 @@ +package com.appsflyer.appsflyersdk; + +public final class LogMessages { + + // Prevent the instantiation of this utilities class. + private LogMessages() { + throw new IllegalStateException("LogMessages class should not be instantiated"); + } + + public static final String METHOD_CHANNEL_IS_NULL = "mMethodChannel is null, cannot invoke the callback"; + public static final String ACTIVITY_NOT_ATTACHED_TO_ENGINE = "Activity isn't attached to the flutter engine"; +} From 5dab6aa5cd600302ace81b3251d3061580be38ca Mon Sep 17 00:00:00 2001 From: Dani Koza <103039399+Dani-Koza-AF@users.noreply.github.com> Date: Mon, 2 Dec 2024 14:33:35 +0200 Subject: [PATCH 11/42] Delivery 76214/update purchase connector version (#363) * bumped up PC versions * set back ios version --- android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/build.gradle b/android/build.gradle index d6077efe..c7144e61 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -50,6 +50,6 @@ dependencies { implementation 'com.android.installreferrer:installreferrer:2.2' // implementation 'androidx.core:core-ktx:1.13.1' if (includeConnector){ - implementation 'com.appsflyer:purchase-connector:2.0.1' + implementation 'com.appsflyer:purchase-connector:2.1.0' } } \ No newline at end of file From 292251147ec4f4b93c51af9f4a56bd814e0ad7a1 Mon Sep 17 00:00:00 2001 From: Dani-Koza-AF Date: Mon, 13 Jan 2025 11:50:27 +0200 Subject: [PATCH 12/42] adding a note on the supported StoreKit to docs --- doc/PurchaseConnector.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/PurchaseConnector.md b/doc/PurchaseConnector.md index b72c61b6..b6cde59a 100644 --- a/doc/PurchaseConnector.md +++ b/doc/PurchaseConnector.md @@ -44,7 +44,7 @@ support@appsflyer.com The Purchase Connector feature of the AppsFlyer SDK depends on specific libraries provided by Google and Apple for managing in-app purchases: - For Android, it depends on the [Google Play Billing Library](https://developer.android.com/google/play/billing/integrate) (Supported versions: 5.x.x - 6.x.x). -- For iOS, it depends on [StoreKit](https://developer.apple.com/documentation/storekit). +- For iOS, it depends on [StoreKit](https://developer.apple.com/documentation/storekit). (Supported version is StoreKit V1) However, these dependencies aren't actively included with the SDK. This means that the responsibility of managing these dependencies and including the necessary libraries in your project falls on you as the consumer of the SDK. From cc74ddb4aa7248325a629f5366ad2338951c5a83 Mon Sep 17 00:00:00 2001 From: Dani-Koza-AF Date: Mon, 13 Jan 2025 15:26:32 +0200 Subject: [PATCH 13/42] typo fix --- lib/src/appsflyer_options.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/appsflyer_options.dart b/lib/src/appsflyer_options.dart index 2e6c66b5..240f5556 100644 --- a/lib/src/appsflyer_options.dart +++ b/lib/src/appsflyer_options.dart @@ -15,7 +15,7 @@ class AppsFlyerOptions { /// Requires [afDevKey] and [appId] as mandatory Named parameters. /// All other parameters are optional, it's allows greater flexibility /// when invoking the constructor. - /// When manual start is true the startSDK must be called + /// When [manualStart] is true the startSDK method must be called AppsFlyerOptions({ required this.afDevKey, this.showDebug = false, From b27f469d22adbf94284ff0c3bfa40d37274d5523 Mon Sep 17 00:00:00 2001 From: Dani-Koza-AF Date: Thu, 16 Jan 2025 09:57:05 +0200 Subject: [PATCH 14/42] Squashed commit of the following: commit fa71c2340f31d72307646fe5966cf4dc0bbf098a Merge: f420460 4f4ee27 Author: Dani-Koza-AF Date: Thu Oct 31 15:16:50 2024 +0200 Merge remote-tracking branch 'origin/development' into development commit f4204607c4d976eb2cf440772c0c6069cd6767bc Author: Dani Koza <103039399+Dani-Koza-AF@users.noreply.github.com> Date: Wed Oct 30 15:50:41 2024 +0200 Releases/6.x.x/6.15.x/6.15.2 rc1 (#358) * fixed the issue with MediationNetwork enums on Android * Added a usage example of the logAdRevenue api for testing. * Fix to the NullPointerException some clients face. * Versioning and change log commit 4f4ee27863c101d49fba53b9248524ffc3a5bbad Merge: a58a49b 25fb530 Author: Dani Koza <103039399+Dani-Koza-AF@users.noreply.github.com> Date: Wed Oct 30 14:47:17 2024 +0200 Merge pull request #357 from AppsFlyerSDK/DELIVERY-63011/fix-android-null-pointer-exception This should fix the NullPointerException commit 25fb530b85c2dd46e5fdcbac85784067f89514f4 Author: Dani-Koza-AF Date: Wed Oct 30 14:26:26 2024 +0200 This should fix the NullPointerException commit a58a49bce23f3b07f84fbe453016bf2e180ae2a0 Merge: 6213341 b85b1a4 Author: Dani-Koza-AF Date: Mon Oct 28 12:30:10 2024 +0200 Merge remote-tracking branch 'origin/development' into development commit b85b1a45710be8db55f39ec4605f971db2eed501 Merge: 82764a4 6529458 Author: Dani Koza <103039399+Dani-Koza-AF@users.noreply.github.com> Date: Mon Oct 28 12:15:32 2024 +0200 Merge pull request #353 from AppsFlyerSDK/dev/DELIVERY-71973/mediation-network-value-fix Fixed the issue with MediationNetwork enums on Android commit 6529458e88ea70e2d52bdd981b37640906c414a4 Author: Dani-Koza-AF Date: Sun Oct 27 17:47:00 2024 +0200 fixed the issue with MediationNetwork enums on Android - added a usage example of the api for testing. commit 621334106fe6b7dfc7ed0b0c5a05d524cfa73d8b Merge: 3272d7e 82764a4 Author: Dani-Koza-AF <103039399+Dani-Koza-AF@users.noreply.github.com> Date: Wed Sep 4 15:51:28 2024 +0300 Merge pull request #338 from AppsFlyerSDK/releases/6.x.x/6.15.x/6.15.1-rc1 Releases/6.x.x/6.15.x/6.15.1 rc1 commit 82764a4eed5f8c61682bc71ea94442c1fd4c77e9 Merge: 3272d7e 6b76d63 Author: Dani-Koza-AF <103039399+Dani-Koza-AF@users.noreply.github.com> Date: Wed Sep 4 15:15:03 2024 +0300 Merge pull request #337 from AppsFlyerSDK/dev/DELIVERY-67805/Update-Plugin-to-v6.15.1 Update plugin to v6.15.1 commit 6b76d63952dc79e649eb35338b1bbc54d2e59611 Author: Dani-Koza-AF Date: Wed Sep 4 15:13:04 2024 +0300 Added missing info in docs commit e7d4dc6b0143cb89ab00d712422f03c47d0593ac Author: Dani-Koza-AF Date: Wed Sep 4 14:17:28 2024 +0300 Added documentation commit 9f90c8e3cb036218a091d47a56acce0aeff2ba9c Author: Dani-Koza-AF Date: Tue Sep 3 17:48:33 2024 +0300 Improvement of Android side impl commit 079ccadb4a616740ced92fd2c20090838d638ce0 Author: Dani-Koza-AF Date: Tue Sep 3 17:22:48 2024 +0300 iOS side impl - Helper func to get the correct enum properly. - requireNonNullArgumentWithCall to make sure we actually get the arguments. - Lots of null safety checks due to testing failures encountered. commit 4a3a0d641c5fb5500d320e0d59f5520f926af89d Author: Dani-Koza-AF Date: Mon Sep 2 14:48:30 2024 +0300 Android side impl - Flutter didn't like the fact that we pass enums, had to change mediation network to String, handled later on native side. - Added an helper method to ensure null safety, hopefully will be embraced by other method in the future. commit d74054e69220c898241ba3fd329419ce8d8ebf85 Author: Dani-Koza-AF Date: Sun Sep 1 16:43:24 2024 +0300 flutter side impl - New Enum introduced. - New API logAdRevenue. - New AdRevenueData class. - Upgraded Dart SDK versions limits a bit to start from 2.17.0 . commit 3272d7e6d9620f2890e4a36c26ffde6ece2b9c2a Merge: 95a4348 248dcf5 Author: liaz-af <61788924+liaz-af@users.noreply.github.com> Date: Mon Aug 19 22:35:24 2024 +0300 Merge pull request #336 from dori-af/dori/udl-note Deep link UDL - added a note commit 248dcf582d27d62b1c497e6ffd9af931f781e241 Author: Dori Frost Date: Sun Aug 18 16:35:07 2024 +0300 Deep link UDL - added a note Per Slack: https://appsflyer.slack.com/archives/C5RDRS58X/p1723186908673099 --- CHANGELOG.md | 10 ++ README.md | 4 +- android/build.gradle | 4 +- .../appsflyersdk/AppsFlyerConstants.java | 46 ++++---- .../appsflyersdk/AppsflyerSdkPlugin.java | 101 +++++++++++++---- .../appsflyer/appsflyersdk/LogMessages.java | 12 ++ doc/API.md | 101 ++++++++++++++++- doc/DeepLink.md | 4 + example/lib/home_container.dart | 16 +++ example/lib/main_page.dart | 32 ++++-- ios/Classes/AppsflyerSdkPlugin.h | 2 +- ios/Classes/AppsflyerSdkPlugin.m | 103 ++++++++++++++++-- ios/appsflyer_sdk.podspec | 8 +- lib/appsflyer_sdk.dart | 3 +- lib/src/appsflyer_ad_revenue_data.dart | 27 +++++ lib/src/appsflyer_constants.dart | 55 +++++++++- lib/src/appsflyer_sdk.dart | 6 + pubspec.yaml | 4 +- 18 files changed, 457 insertions(+), 81 deletions(-) create mode 100644 android/src/main/java/com/appsflyer/appsflyersdk/LogMessages.java create mode 100644 lib/src/appsflyer_ad_revenue_data.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 67e25590..f41636bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,14 @@ # Versions +## 6.15.2 +- Fixed NullPointerException issue on Android that some clients had. +- Fixed Android MediationNetwork enum issue. +- Update iOS version to 6.15.3 +- Update Android version to 6.15.2 +## 6.15.1 +- Implementation of the new logAdRevenue API for iOS and Android +- Documentation update for the new logAdRevenue API +- Update iOS version to 6.15.1 +- Update Android version to 6.15.1 ## 6.14.3 - Fixed mapOptions issue with manualStart - Inherit Privacy Manifest from the native iOS SDK via Cocoapods diff --git a/README.md b/README.md index 36a0a8a1..0d9980d4 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ ### This plugin is built for -- Android AppsFlyer SDK **v6.14.0** -- iOS AppsFlyer SDK **v6.14.3** +- Android AppsFlyer SDK **v6.15.2** +- iOS AppsFlyer SDK **v6.15.3** ## ❗❗ Breaking changes when updating to v6.x.x❗❗ diff --git a/android/build.gradle b/android/build.gradle index c7144e61..84a1590a 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -46,10 +46,10 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.appcompat:appcompat:1.0.0' - implementation 'com.appsflyer:af-android-sdk:6.14.0' + implementation 'com.appsflyer:af-android-sdk:6.15.2' implementation 'com.android.installreferrer:installreferrer:2.2' // implementation 'androidx.core:core-ktx:1.13.1' - if (includeConnector){ + if (includeConnector) { implementation 'com.appsflyer:purchase-connector:2.1.0' } } \ No newline at end of file diff --git a/android/src/main/java/com/appsflyer/appsflyersdk/AppsFlyerConstants.java b/android/src/main/java/com/appsflyer/appsflyersdk/AppsFlyerConstants.java index df18069d..f96251cb 100644 --- a/android/src/main/java/com/appsflyer/appsflyersdk/AppsFlyerConstants.java +++ b/android/src/main/java/com/appsflyer/appsflyersdk/AppsFlyerConstants.java @@ -1,31 +1,33 @@ package com.appsflyer.appsflyersdk; -public class AppsFlyerConstants { - final static String PLUGIN_VERSION = "6.14.3"; - final static String AF_APP_INVITE_ONE_LINK = "appInviteOneLink"; - final static String AF_HOST_PREFIX = "hostPrefix"; - final static String AF_HOST_NAME = "hostName"; - final static String AF_IS_DEBUG = "isDebug"; - final static String AF_MANUAL_START = "manualStart"; - final static String AF_DEV_KEY = "afDevKey"; - final static String AF_EVENT_NAME = "eventName"; - final static String AF_EVENT_VALUES = "eventValues"; - final static String AF_ON_INSTALL_CONVERSION_DATA_LOADED = "onInstallConversionDataLoaded"; - final static String AF_ON_APP_OPEN_ATTRIBUTION = "onAppOpenAttribution"; - final static String AF_SUCCESS = "success"; - final static String AF_FAILURE = "failure"; - final static String AF_GCD = "GCD"; - final static String AF_UDL = "UDL"; - final static String AF_VALIDATE_PURCHASE = "validatePurchase"; - final static String AF_GCD_CALLBACK = "onInstallConversionData"; - final static String AF_OAOA_CALLBACK = "onAppOpenAttribution"; - final static String AF_UDL_CALLBACK = "onDeepLinking"; - final static String DISABLE_ADVERTISING_IDENTIFIER = "disableAdvertisingIdentifier"; +public final class AppsFlyerConstants { + final static String PLUGIN_VERSION = "6.15.1"; + final static String AF_APP_INVITE_ONE_LINK = "appInviteOneLink"; + final static String AF_HOST_PREFIX = "hostPrefix"; + final static String AF_HOST_NAME = "hostName"; + final static String AF_IS_DEBUG = "isDebug"; + final static String AF_MANUAL_START = "manualStart"; + final static String AF_DEV_KEY = "afDevKey"; + final static String AF_EVENT_NAME = "eventName"; + final static String AF_EVENT_VALUES = "eventValues"; + final static String AF_ON_INSTALL_CONVERSION_DATA_LOADED = "onInstallConversionDataLoaded"; + final static String AF_ON_APP_OPEN_ATTRIBUTION = "onAppOpenAttribution"; + final static String AF_SUCCESS = "success"; + final static String AF_FAILURE = "failure"; + final static String AF_GCD = "GCD"; + final static String AF_UDL = "UDL"; + final static String AF_VALIDATE_PURCHASE = "validatePurchase"; + final static String AF_GCD_CALLBACK = "onInstallConversionData"; + final static String AF_OAOA_CALLBACK = "onAppOpenAttribution"; + final static String AF_UDL_CALLBACK = "onDeepLinking"; + final static String DISABLE_ADVERTISING_IDENTIFIER = "disableAdvertisingIdentifier"; final static String AF_EVENTS_CHANNEL = "af-events"; final static String AF_METHOD_CHANNEL = "af-api"; final static String AF_PURCHASE_CONNECTOR_CHANNEL = "af-purchase-connector"; final static String AF_CALLBACK_CHANNEL = "callbacks"; - final static String AF_BROADCAST_ACTION_NAME = "com.appsflyer.appsflyersdk"; + final static String AF_BROADCAST_ACTION_NAME = "com.appsflyer.appsflyersdk"; + + final static String AF_PLUGIN_TAG = "AppsFlyer_FlutterPlugin"; } diff --git a/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java b/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java index 60d4340f..ed342673 100644 --- a/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java +++ b/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java @@ -9,12 +9,14 @@ import android.os.Looper; import android.util.Log; +import com.appsflyer.AFAdRevenueData; import com.appsflyer.AFLogger; import com.appsflyer.AppsFlyerConsent; import com.appsflyer.AppsFlyerConversionListener; import com.appsflyer.AppsFlyerInAppPurchaseValidatorListener; import com.appsflyer.AppsFlyerLib; import com.appsflyer.AppsFlyerProperties; +import com.appsflyer.MediationNetwork; import com.appsflyer.deeplink.DeepLinkListener; import com.appsflyer.deeplink.DeepLinkResult; import com.appsflyer.share.CrossPromotionHelper; @@ -47,6 +49,7 @@ import static com.appsflyer.appsflyersdk.AppsFlyerConstants.AF_EVENTS_CHANNEL; import static com.appsflyer.appsflyersdk.AppsFlyerConstants.AF_FAILURE; +import static com.appsflyer.appsflyersdk.AppsFlyerConstants.AF_PLUGIN_TAG; import static com.appsflyer.appsflyersdk.AppsFlyerConstants.AF_SUCCESS; /** @@ -201,7 +204,7 @@ private void startListening(Object arguments, Result rawResult) { @Override public void onMethodCall(MethodCall call, Result result) { if (activity == null) { - Log.d("AppsFlyer", "Activity isn't attached to the flutter engine"); + Log.d(AF_PLUGIN_TAG, LogMessages.ACTIVITY_NOT_ATTACHED_TO_ENGINE); return; } final String method = call.method; @@ -344,6 +347,9 @@ public void onMethodCall(MethodCall call, Result result) { case "addPushNotificationDeepLinkPath": addPushNotificationDeepLinkPath(call, result); break; + case "logAdRevenue": + logAdRevenue(call, result); + break; default: result.notImplemented(); break; @@ -357,11 +363,11 @@ private void performOnDeepLinking(MethodCall call, Result result) { AppsFlyerLib.getInstance().performOnDeepLinking(intent, mApplication); result.success(null); } else { - Log.d("AppsFlyer", "performOnDeepLinking: intent is null!"); + Log.d(AF_PLUGIN_TAG, "performOnDeepLinking: intent is null!"); result.error("NO_INTENT", "The intent is null", null); } } else { - Log.d("AppsFlyer", "performOnDeepLinking: activity is null!"); + Log.d(AF_PLUGIN_TAG, "performOnDeepLinking: activity is null!"); result.error("NO_ACTIVITY", "The current activity is null", null); } } @@ -374,34 +380,37 @@ private void anonymizeUser(MethodCall call, Result result) { private void startSDKwithHandler(MethodCall call, final Result result) { try { - final AppsFlyerLib instance = AppsFlyerLib.getInstance(); - instance.start(activity, null, new AppsFlyerRequestListener() { + final AppsFlyerLib appsFlyerLib = AppsFlyerLib.getInstance(); + + appsFlyerLib.start(activity, null, new AppsFlyerRequestListener() { @Override public void onSuccess() { - uiThreadHandler.post(new Runnable() { - @Override - public void run() { - mMethodChannel.invokeMethod("onSuccess", null); - } - }); + if (mMethodChannel != null) { + uiThreadHandler.post(() -> mMethodChannel.invokeMethod("onSuccess", null)); + } else { + Log.e(AF_PLUGIN_TAG, LogMessages.METHOD_CHANNEL_IS_NULL); + result.error("NULL_OBJECT", LogMessages.METHOD_CHANNEL_IS_NULL, null); + } } @Override public void onError(final int errorCode, final String errorMessage) { - uiThreadHandler.post(new Runnable() { - @Override - public void run() { + if (mMethodChannel != null) { + uiThreadHandler.post(() -> { HashMap errorDetails = new HashMap<>(); errorDetails.put("errorCode", errorCode); errorDetails.put("errorMessage", errorMessage); mMethodChannel.invokeMethod("onError", errorDetails); - } - }); + }); + } else { + Log.e(AF_PLUGIN_TAG, LogMessages.METHOD_CHANNEL_IS_NULL); + result.error("NULL_OBJECT", LogMessages.METHOD_CHANNEL_IS_NULL, null); + } } }); result.success(null); - } catch (Exception e) { - result.error("UNEXPECTED_ERROR", e.getMessage(), null); + } catch (Throwable t) { + result.error("UNEXPECTED_ERROR", t.getMessage(), null); } } @@ -527,14 +536,14 @@ private void sendPushNotificationData(MethodCall call, Result result) { Bundle bundle; if (pushPayload == null) { - Log.d("AppsFlyer", "Push payload is null"); + Log.d(AF_PLUGIN_TAG, "Push payload is null"); return; } try { bundle = this.jsonToBundle(new JSONObject(pushPayload)); } catch (JSONException e) { - Log.d("AppsFlyer", "Can't parse pushPayload to bundle"); + Log.d(AF_PLUGIN_TAG, "Can't parse pushPayload to bundle"); return; } @@ -552,7 +561,7 @@ private void sendPushNotificationData(MethodCall call, Result result) { } if (errorMsg != null) { - Log.d("AppsFlyer", errorMsg); + Log.d(AF_PLUGIN_TAG, errorMsg); return; } @@ -954,6 +963,56 @@ private void logEvent(MethodCall call, MethodChannel.Result result) { result.success(true); } + private void logAdRevenue(MethodCall call, Result result) { + try { + String monetizationNetwork = requireNonNullArgument(call, "monetizationNetwork"); + String currencyIso4217Code = requireNonNullArgument(call, "currencyIso4217Code"); + double revenue = requireNonNullArgument(call, "revenue"); + String mediationNetworkString = requireNonNullArgument(call, "mediationNetwork"); + + MediationNetwork mediationNetwork = MediationNetwork.valueOf(mediationNetworkString.toUpperCase()); + + // No null check for additionalParameters since it's acceptable for it to be null (optional data) + Map additionalParameters = call.argument("additionalParameters"); + + AFAdRevenueData adRevenueData = new AFAdRevenueData( + monetizationNetwork, + mediationNetwork, + currencyIso4217Code, + revenue + ); + + AppsFlyerLib.getInstance().logAdRevenue(adRevenueData, additionalParameters); + result.success(true); + + } catch (IllegalArgumentException e) { + // The IllegalArgumentException could come from either requireNonNullArgument or valueOf methods. + result.error("INVALID_ARGUMENT_PROVIDED", e.getMessage(), null); + } catch (Throwable t) { + result.error("UNEXPECTED_ERROR", "[logAdRevenue]: An unexpected error occurred: " + t.getMessage(), null); + Log.e(AF_PLUGIN_TAG, "Unexpected exception occurred: [logAdRevenue]", t); + } + } + + /** + * Utility method to ensure that an argument with the specified name is not null. + * If the argument is null, this method will throw an IllegalArgumentException. + * The calling method can then terminate immediately without further processing. + * + * @param call The MethodCall from Flutter, containing all the arguments. + * @param argumentName The name of the argument expected in the MethodCall. + * @param The type of the argument being checked for nullity. + * @return The argument value if it is not null; throw IllegalArgumentException otherwise. + */ + private T requireNonNullArgument(MethodCall call, String argumentName) throws IllegalArgumentException { + T argument = call.argument(argumentName); + if (argument == null) { + Log.e(AF_PLUGIN_TAG, "Exception occurred when trying to: " + call.method + "->" + argumentName + " must not be null"); + throw new IllegalArgumentException("[" + call.method + "]: " + argumentName + " must not be null"); + } + return argument; + } + //RD-65582 private void sendCachedCallbacksToDart() { if (cachedDeepLinkResult != null) { diff --git a/android/src/main/java/com/appsflyer/appsflyersdk/LogMessages.java b/android/src/main/java/com/appsflyer/appsflyersdk/LogMessages.java new file mode 100644 index 00000000..cc363ef3 --- /dev/null +++ b/android/src/main/java/com/appsflyer/appsflyersdk/LogMessages.java @@ -0,0 +1,12 @@ +package com.appsflyer.appsflyersdk; + +public final class LogMessages { + + // Prevent the instantiation of this utilities class. + private LogMessages() { + throw new IllegalStateException("LogMessages class should not be instantiated"); + } + + public static final String METHOD_CHANNEL_IS_NULL = "mMethodChannel is null, cannot invoke the callback"; + public static final String ACTIVITY_NOT_ATTACHED_TO_ENGINE = "Activity isn't attached to the flutter engine"; +} diff --git a/doc/API.md b/doc/API.md index 390ae9e9..94cc932b 100644 --- a/doc/API.md +++ b/doc/API.md @@ -4,6 +4,8 @@ ## Types - [AppsFlyerOptions](#appsflyer-options) +- [AdRevenueData](#AdRevenueData) +- [AFMediationNetwork](#AFMediationNetwork) ## Methods - [initSdk](#initSdk) @@ -52,11 +54,12 @@ - [getOutOfStore](#getOutOfStore) - [setDisableNetworkData](#setDisableNetworkData) - [performOnDeepLinking](#performondeeplinking) +- [logAdRevenue](#logAdRevenue) - Since 6.15.1 --- -##### **`AppsflyerSdk(Map options)`** +##### **`AppsflyerSdk(Map options)`** | parameter | type | description | | --------- | ----- | ----------------- | @@ -116,6 +119,42 @@ Once `AppsflyerSdk` object is created, you can call `initSdk` method. --- +##### **`AdRevenueData`** + +| parameter | type | description | +| --------- | ------------------ | ----------------- | +| `monetizationNetwork` | `String` | | +| `mediationNetwork` | `String` | value must be taken from `AFMediationNetwork` | +| `currencyIso4217Code` | `String` | | +| `revenue` | `double` | | +| `additionalParameters` | `Map?` | | + +--- + +##### **`AFMediationNetwork`** +an enumeration that includes the supported mediation networks by AppsFlyer. + + +| networks | +| -------- | +| ironSource +applovinMax +googleAdMob +fyber +appodeal +admost +topon +tradplus +yandex +chartboost +unity +toponPte +customMediation +directMonetizationNetwork | + +--- + + ##### **`initSdk({bool registerConversionDataCallback, bool registerOnAppOpenAttributionCallback}) async` (Changed in 1.2.2)** initialize the SDK, using the options initialized from the constructor| @@ -561,7 +600,7 @@ This call matches the following payload structure: 1. First define the Onelink ID (find it in the AppsFlyer dashboard in the onelink section: - **`Future setAppInviteOneLinkID(String oneLinkID, Function callback)`** +**`Future setAppInviteOneLinkID(String oneLinkID, Function callback)`** 2. Set the AppsFlyerInviteLinkParams class to set the query params in the user invite link: @@ -579,7 +618,7 @@ class AppsFlyerInviteLinkParams { 3. Call the generateInviteLink API to generate the user invite link. Use the success and error callbacks for handling. - **`void generateInviteLink(AppsFlyerInviteLinkParams parameters, Function success, Function error)`** +**`void generateInviteLink(AppsFlyerInviteLinkParams parameters, Function success, Function error)`** _Example:_ @@ -653,7 +692,7 @@ appsFlyerSdk.setCurrentDeviceLanguage("en"); --- ** `void setSharingFilterForPartners(List partners)`** -`setSharingFilter` & `setSharingFilterForAllPartners` APIs were deprecated! +`setSharingFilter` & `setSharingFilterForAllPartners` APIs were deprecated! Use `setSharingFilterForPartners` instead. @@ -672,9 +711,9 @@ appsFlyerSdk.setSharingFilterForPartners(['googleadwords_int', 'all']); --- ** `void setOneLinkCustomDomain(List brandDomains)`** -Use this API in order to set branded domains. +Use this API in order to set branded domains. -Find more information in the [following article on branded domains](https://support.appsflyer.com/hc/en-us/articles/360002329137-Implementing-Branded-Links). +Find more information in the [following article on branded domains](https://support.appsflyer.com/hc/en-us/articles/360002329137-Implementing-Branded-Links). _Example:_ ```dart @@ -823,3 +862,53 @@ Note:
This API will trigger the `appsflyerSdk.onDeepLink` callback. In the fo _appsflyerSdk.startSDK(); } ``` + +--- + +### **
`void logAdRevenue(AdRevenueData adRevenueData)`** + +The logAdRevenue API is designed to simplify the process of logging ad revenue events to AppsFlyer from your Flutter application. This API tracks revenue generated from advertisements, enriching your monetization analytics. Below you will find instructions on how to use this API correctly, along with detailed descriptions and examples for various input scenarios. + +### **Usage:** +To use the logAdRevenue method, you must: + +1. Prepare an instance of `AdRevenueData` with the required information about the ad revenue event. +1. Call `logAdRevenue` with the `AdRevenueData` instance. + +**AdRevenueData Class** +[AdRevenueData](#AdRevenueData) is a data class representing all the relevant information about an ad revenue event: + +* `monetizationNetwork`: The source network from which the revenue was generated (e.g., AdMob, Unity Ads). +* `mediationNetwork`: The mediation platform managing the ad (use AFMediationNetwork enum for supported networks). +* `currencyIso4217Code`: The ISO 4217 currency code representing the currency of the revenue amount (e.g., "USD", "EUR"). +* `revenue`: The amount of revenue generated from the ad. +* `additionalParameters`: Additional parameters related to the ad revenue event (optional). + + +**AFMediationNetwork Enum** +[AFMediationNetwork](#AFMediationNetwork) is an enumeration that includes the supported mediation networks by AppsFlyer. It's important to use this enum to ensure you provide a valid network identifier to the logAdRevenue API. + +### Example: +```dart +// Instantiate AdRevenueData with the ad revenue details. +AdRevenueData adRevenueData = AdRevenueData( + monetizationNetwork: "GoogleAdMob", // Replace with your actual monetization network. + mediationNetwork: AFMediationNetwork.applovinMax.value, // Use the value from the enum. + currencyIso4217Code: "USD", + revenue: 1.23, + additionalParameters: { + // Optional additional parameters can be added here. This is an example, can be discard if not needed. + 'adUnitId': 'ca-app-pub-XXXX/YYYY', + 'ad_network_click_id': '12345' + } +); + +// Log the ad revenue event. +logAdRevenue(adRevenueData); +``` + +**Additional Points** +* Mediation network input must be from the provided [AFMediationNetwork](#AFMediationNetwork) + enum to ensure proper processing by AppsFlyer. For instance, use `AFMediationNetwork.googleAdMob.value` to denote Google AdMob as the Mediation Network. +* The `additionalParameters` map is optional. Use it to pass any extra information you have regarding the ad revenue event; this information could be useful for more refined analytics. +* Make sure the `currencyIso4217Code` adheres to the appropriate standard. Misconfigured currency code may result in incorrect revenue tracking. \ No newline at end of file diff --git a/doc/DeepLink.md b/doc/DeepLink.md index a88bc320..a356e183 100644 --- a/doc/DeepLink.md +++ b/doc/DeepLink.md @@ -58,6 +58,10 @@ appsflyerSdk.onAppOpenAttribution((res){ ### 3. Unified deep linking +> 📘 **UDL privacy protection** +> +> For new users, the UDL method only returns parameters relevant to deferred deep linking: `deep_link_value` and `deep_link_sub1` to `deep_link_sub10`. If you try to get any other parameters (`media_source`, `campaign`, `af_sub1-5`, etc.), they return `null`. + The flow works as follows: 1. User clicks the OneLink short URL. diff --git a/example/lib/home_container.dart b/example/lib/home_container.dart index 43d8d82a..a4748309 100644 --- a/example/lib/home_container.dart +++ b/example/lib/home_container.dart @@ -7,12 +7,14 @@ import 'utils.dart'; class HomeContainer extends StatefulWidget { final Map onData; final Future Function(String, Map) logEvent; + final void Function() logAdRevenueEvent; Object deepLinkData; HomeContainer({ required this.onData, required this.deepLinkData, required this.logEvent, + required this.logAdRevenueEvent, }); @override @@ -136,6 +138,20 @@ class _HomeContainerState extends State { ), ), ), + ElevatedButton( + onPressed: () { + widget.logAdRevenueEvent(); + }, + child: Text("Trigger AdRevenue Event"), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10), + textStyle: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ), ], ), ) diff --git a/example/lib/main_page.dart b/example/lib/main_page.dart index e0da017e..70ce33b7 100644 --- a/example/lib/main_page.dart +++ b/example/lib/main_page.dart @@ -124,6 +124,7 @@ class MainPageState extends State { onData: _gcd, deepLinkData: _deepLinkData, logEvent: logEvent, + logAdRevenueEvent: logAdRevenueEvent, ), ), ElevatedButton( @@ -133,7 +134,8 @@ class MainPageState extends State { showMessage("AppsFlyer SDK initialized successfully."); }, onError: (int errorCode, String errorMessage) { - showMessage("Error initializing AppsFlyer SDK: Code $errorCode - $errorMessage"); + showMessage( + "Error initializing AppsFlyer SDK: Code $errorCode - $errorMessage"); }, ); }, @@ -158,13 +160,29 @@ class MainPageState extends State { return logResult; } + void logAdRevenueEvent() { + try { + Map customParams = { + 'ad_platform': 'Admob', + 'ad_currency': 'USD', + }; + + AdRevenueData adRevenueData = AdRevenueData( + monetizationNetwork: 'SpongeBob', + mediationNetwork: AFMediationNetwork.googleAdMob.value, + currencyIso4217Code: 'USD', + revenue: 100.3, + additionalParameters: customParams); + _appsflyerSdk.logAdRevenue(adRevenueData); + print("Ad Revenue event logged with no errors"); + } catch (e) { + print("Failed to log event: $e"); + } + } + void showMessage(String message) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar( - content: - Text(message), + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(message), )); } } - - diff --git a/ios/Classes/AppsflyerSdkPlugin.h b/ios/Classes/AppsflyerSdkPlugin.h index a6055a9b..0ae703a8 100644 --- a/ios/Classes/AppsflyerSdkPlugin.h +++ b/ios/Classes/AppsflyerSdkPlugin.h @@ -18,7 +18,7 @@ @end // Appsflyer JS objects -#define kAppsFlyerPluginVersion @"6.14.3" +#define kAppsFlyerPluginVersion @"6.15.1" #define afDevKey @"afDevKey" #define afAppId @"afAppId" #define afIsDebug @"isDebug" diff --git a/ios/Classes/AppsflyerSdkPlugin.m b/ios/Classes/AppsflyerSdkPlugin.m index 234ad807..fc7c9a9a 100644 --- a/ios/Classes/AppsflyerSdkPlugin.m +++ b/ios/Classes/AppsflyerSdkPlugin.m @@ -99,7 +99,7 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { }else if([@"setIsUpdate" isEqualToString:call.method]){ // }else if([@"setCustomerUserId" isEqualToString:call.method]){ - [self setCustomerUserId:call result:result]; + [self setCustomerUserId:call result:result]; }else if([@"setCustomerIdAndLogSession" isEqualToString:call.method]){ [self setCustomerUserId:call result:result]; }else if([@"setCurrencyCode" isEqualToString:call.method ]){ @@ -135,7 +135,7 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { }else if([@"setOneLinkCustomDomain" isEqualToString:call.method]){ [self setOneLinkCustomDomain:call result:result]; }else if([@"setPushNotification" isEqualToString:call.method]){ - [self setPushNotification:call result:result]; + [self setPushNotification:call result:result]; }else if([@"sendPushNotificationData" isEqualToString:call.method]){ [self sendPushNotificationData:call result:result]; }else if([@"useReceiptValidationSandbox" isEqualToString:call.method]){ @@ -162,6 +162,8 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { [self enableTCFDataCollection:call result:result]; }else if([@"setConsentData" isEqualToString:call.method]){ [self setConsentData:call result:result]; + }else if([@"logAdRevenue" isEqualToString:call.method]){ + [self logAdRevenue:call result:result]; } else{ result(FlutterMethodNotImplemented); @@ -194,22 +196,105 @@ - (void)startSDK:(FlutterMethodCall*)call result:(FlutterResult)result { - (void)setConsentData:(FlutterMethodCall*)call result:(FlutterResult)result { NSDictionary* consentDict = call.arguments[@"consentData"]; - + BOOL isUserSubjectToGDPR = [consentDict[@"isUserSubjectToGDPR"] boolValue]; BOOL hasConsentForDataUsage = [consentDict[@"hasConsentForDataUsage"] boolValue]; BOOL hasConsentForAdsPersonalization = [consentDict[@"hasConsentForAdsPersonalization"] boolValue]; AppsFlyerConsent *consentData; if(isUserSubjectToGDPR){ consentData = [[AppsFlyerConsent alloc] initForGDPRUserWithHasConsentForDataUsage:hasConsentForDataUsage - hasConsentForAdsPersonalization:hasConsentForAdsPersonalization]; + hasConsentForAdsPersonalization:hasConsentForAdsPersonalization]; }else{ consentData = [[AppsFlyerConsent alloc] initNonGDPRUser]; } - + [[AppsFlyerLib shared] setConsentData:consentData]; result(nil); } +- (void)logAdRevenue:(FlutterMethodCall*)call result:(FlutterResult)result { + @try { + NSString *monetizationNetwork = [self requireNonNullArgumentWithCall:call result:result argumentName:@"monetizationNetwork" errorCode:@"NULL_MONETIZATION_NETWORK"]; + if (monetizationNetwork == nil) return; + + NSString *currencyIso4217Code = [self requireNonNullArgumentWithCall:call result:result argumentName:@"currencyIso4217Code" errorCode:@"NULL_CURRENCY_CODE"]; + if (currencyIso4217Code == nil) return; + + NSNumber *revenueValue = [self requireNonNullArgumentWithCall:call result:result argumentName:@"revenue" errorCode:@"NULL_REVENUE"]; + if (revenueValue == nil) return; + + NSString *mediationNetworkString = [self requireNonNullArgumentWithCall:call result:result argumentName:@"mediationNetwork" errorCode:@"NULL_MEDIATION_NETWORK"]; + if (mediationNetworkString == nil) return; + + // Fetching the actual mediationNetwork Enum + AppsFlyerAdRevenueMediationNetworkType mediationNetwork = [self getEnumValueFromString:mediationNetworkString]; + if (mediationNetwork == -1) { //mediation network not found. + result([FlutterError errorWithCode:@"INVALID_MEDIATION_NETWORK" + message:@"The provided mediation network is not supported." + details:nil]); + return; + } + + NSDictionary *additionalParameters = call.arguments[@"additionalParameters"]; + if ([additionalParameters isEqual:[NSNull null]]) { + additionalParameters = nil; // Set to nil to avoid sending NSNull to the SDK which cannot be proseesed. + } + + AFAdRevenueData *adRevenueData = [[AFAdRevenueData alloc] + initWithMonetizationNetwork:monetizationNetwork + mediationNetwork:mediationNetwork + currencyIso4217Code:currencyIso4217Code + eventRevenue:revenueValue]; + + [[AppsFlyerLib shared] logAdRevenue:adRevenueData additionalParameters:additionalParameters]; + + } @catch (NSException *exception) { + result([FlutterError errorWithCode:@"UNEXPECTED_ERROR" + message:[NSString stringWithFormat:@"[logAdRevenue]: An error occurred retrieving method arguments: %@", exception.reason] + details:nil]); + NSLog(@"AppsFlyer, Exception occurred in [logAdRevenue]: %@", exception.reason); + } + +} + +- (AppsFlyerAdRevenueMediationNetworkType)getEnumValueFromString:(NSString *)mediationNetworkString { + NSDictionary *stringToEnumMap = @{ + @"google_admob": @(AppsFlyerAdRevenueMediationNetworkTypeGoogleAdMob), + @"ironsource": @(AppsFlyerAdRevenueMediationNetworkTypeIronSource), + @"applovin_max": @(AppsFlyerAdRevenueMediationNetworkTypeApplovinMax), + @"fyber": @(AppsFlyerAdRevenueMediationNetworkTypeFyber), + @"appodeal": @(AppsFlyerAdRevenueMediationNetworkTypeAppodeal), + @"admost": @(AppsFlyerAdRevenueMediationNetworkTypeAdmost), + @"topon": @(AppsFlyerAdRevenueMediationNetworkTypeTopon), + @"tradplus": @(AppsFlyerAdRevenueMediationNetworkTypeTradplus), + @"yandex": @(AppsFlyerAdRevenueMediationNetworkTypeYandex), + @"chartboost": @(AppsFlyerAdRevenueMediationNetworkTypeChartBoost), + @"unity": @(AppsFlyerAdRevenueMediationNetworkTypeUnity), + @"topon_pte": @(AppsFlyerAdRevenueMediationNetworkTypeToponPte), + @"custom_mediation": @(AppsFlyerAdRevenueMediationNetworkTypeCustom), + @"direct_monetization_network": @(AppsFlyerAdRevenueMediationNetworkTypeDirectMonetization) + }; + + NSNumber *enumValueNumber = stringToEnumMap[mediationNetworkString]; + if (enumValueNumber) { + return (AppsFlyerAdRevenueMediationNetworkType)[enumValueNumber integerValue]; + } else { + return -1; + } +} + +- (id)requireNonNullArgumentWithCall:(FlutterMethodCall*)call result:(FlutterResult)result argumentName:(NSString *)argumentName errorCode:(NSString *)errorCode { + id value = call.arguments[argumentName]; + if (value == nil) { + result([FlutterError + errorWithCode:errorCode + message:[NSString stringWithFormat:@"%@ must not be null", argumentName] + details:nil]); + NSLog(@"AppsFlyer, %@ must not be null", argumentName); + } + return value; +} + - (void)enableTCFDataCollection:(FlutterMethodCall*)call result:(FlutterResult)result { BOOL shouldCollect = [call.arguments[@"shouldCollect"] boolValue]; [[AppsFlyerLib shared] enableTCFDataCollection:shouldCollect]; @@ -662,7 +747,7 @@ - (void)initSdkWithCall:(FlutterMethodCall*)call result:(FlutterResult)result{ } [[AppsFlyerLib shared] setPluginInfoWith:AFSDKPluginFlutter pluginVersion:kAppsFlyerPluginVersion additionalParams:nil]; - + [AppsFlyerLib shared].appleAppID = appId; [AppsFlyerLib shared].appsFlyerDevKey = devKey; [AppsFlyerLib shared].isDebug = isDebug; @@ -678,12 +763,12 @@ - (void)initSdkWithCall:(FlutterMethodCall*)call result:(FlutterResult)result{ if (timeToWaitForATTUserAuthorization != 0) { [[AppsFlyerLib shared] waitForATTUserAuthorizationWithTimeoutInterval:timeToWaitForATTUserAuthorization]; } - + if (manualStart == NO){ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appDidBecomeActive) name:UIApplicationDidBecomeActiveNotification object:nil]; [[AppsFlyerLib shared] start]; } - + //post notification for the deep link object that the bridge is set and he can handle deep link [AppsFlyerAttribution shared].isBridgeReady = YES; [[NSNotificationCenter defaultCenter] postNotificationName:AF_BRIDGE_SET object:self]; @@ -695,7 +780,7 @@ - (void)initSdkWithCall:(FlutterMethodCall*)call result:(FlutterResult)result{ -(void)logEventWithCall:(FlutterMethodCall*)call result:(FlutterResult)result{ NSString *eventName = call.arguments[afEventName]; NSDictionary *eventValues = call.arguments[afEventValues]; - + // Explicitily setting the values to be nil if call.arguments[afEventValues] returns . if (eventValues == [NSNull null]) { eventValues = nil; diff --git a/ios/appsflyer_sdk.podspec b/ios/appsflyer_sdk.podspec index 53440a63..c48c2b81 100644 --- a/ios/appsflyer_sdk.podspec +++ b/ios/appsflyer_sdk.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'appsflyer_sdk' - s.version = '6.14.3' + s.version = '6.15.3' s.summary = 'AppsFlyer Integration for Flutter' s.description = 'AppsFlyer is the market leader in mobile advertising attribution & analytics, helping marketers to pinpoint their targeting, optimize their ad spend and boost their ROI.' s.homepage = 'https://github.com/AppsFlyerSDK/flutter_appsflyer_sdk' @@ -21,15 +21,15 @@ Pod::Spec.new do |s| ss.source_files = 'Classes/**/*' ss.public_header_files = 'Classes/**/*.h' ss.dependency 'Flutter' - ss.ios.dependency 'AppsFlyerFramework','6.14.3' + ss.ios.dependency 'AppsFlyerFramework','6.15.3' end s.subspec 'PurchaseConnector' do |ss| ss.dependency 'Flutter' - ss.ios.dependency 'PurchaseConnector', '6.14.3' + ss.ios.dependency 'PurchaseConnector', '6.15.3' ss.source_files = 'PurchaseConnector/**/*' ss.public_header_files = 'PurchaseConnector/**/*.h' ss.pod_target_xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) ENABLE_PURCHASE_CONNECTOR=1' } end -end \ No newline at end of file +end diff --git a/lib/appsflyer_sdk.dart b/lib/appsflyer_sdk.dart index 88865558..ff9df5ce 100644 --- a/lib/appsflyer_sdk.dart +++ b/lib/appsflyer_sdk.dart @@ -30,4 +30,5 @@ part 'src/purchase_connector/models/jvm_throwable.dart'; part 'src/purchase_connector/models/ios_error.dart'; part 'src/appsflyer_consent.dart'; part 'src/appsflyer_request_listener.dart'; -part 'appsflyer_sdk.g.dart'; \ No newline at end of file +part 'appsflyer_sdk.g.dart'; +part 'src/appsflyer_ad_revenue_data.dart'; diff --git a/lib/src/appsflyer_ad_revenue_data.dart b/lib/src/appsflyer_ad_revenue_data.dart new file mode 100644 index 00000000..7482cf7b --- /dev/null +++ b/lib/src/appsflyer_ad_revenue_data.dart @@ -0,0 +1,27 @@ +part of appsflyer_sdk; + +class AdRevenueData { + final String monetizationNetwork; + final String mediationNetwork; + final String currencyIso4217Code; + final double revenue; + final Map? additionalParameters; + + AdRevenueData({ + required this.monetizationNetwork, + required this.mediationNetwork, + required this.currencyIso4217Code, + required this.revenue, + this.additionalParameters + }); + + Map toMap() { + return { + 'monetizationNetwork': monetizationNetwork, + 'mediationNetwork': mediationNetwork, + 'currencyIso4217Code': currencyIso4217Code, + 'revenue': revenue, + 'additionalParameters': additionalParameters + }; + } +} diff --git a/lib/src/appsflyer_constants.dart b/lib/src/appsflyer_constants.dart index f6a2a7c2..67ed135d 100644 --- a/lib/src/appsflyer_constants.dart +++ b/lib/src/appsflyer_constants.dart @@ -1,9 +1,6 @@ part of appsflyer_sdk; -enum EmailCryptType { - EmailCryptTypeNone, - EmailCryptTypeSHA256 -} +enum EmailCryptType { EmailCryptTypeNone, EmailCryptTypeSHA256 } class AppsflyerConstants { @@ -56,3 +53,53 @@ class AppsflyerConstants { static const String LOG_IN_APP_KEY = "logInApps"; static const String SANDBOX_KEY = "sandbox"; } + +enum AFMediationNetwork { + ironSource, + applovinMax, + googleAdMob, + fyber, + appodeal, + admost, + topon, + tradplus, + yandex, + chartboost, + unity, + toponPte, + customMediation, + directMonetizationNetwork; + + String get value { + switch (this) { + case AFMediationNetwork.ironSource: + return "ironsource"; + case AFMediationNetwork.applovinMax: + return "applovin_max"; + case AFMediationNetwork.googleAdMob: + return "google_admob"; + case AFMediationNetwork.fyber: + return "fyber"; + case AFMediationNetwork.appodeal: + return "appodeal"; + case AFMediationNetwork.admost: + return "admost"; + case AFMediationNetwork.topon: + return "topon"; + case AFMediationNetwork.tradplus: + return "tradplus"; + case AFMediationNetwork.yandex: + return "yandex"; + case AFMediationNetwork.chartboost: + return "chartboost"; + case AFMediationNetwork.unity: + return "unity"; + case AFMediationNetwork.toponPte: + return "topon_pte"; + case AFMediationNetwork.customMediation: + return "custom_mediation"; + case AFMediationNetwork.directMonetizationNetwork: + return "direct_monetization_network"; + } + } +} diff --git a/lib/src/appsflyer_sdk.dart b/lib/src/appsflyer_sdk.dart index 69522866..c4cde1bd 100644 --- a/lib/src/appsflyer_sdk.dart +++ b/lib/src/appsflyer_sdk.dart @@ -237,6 +237,12 @@ class AppsflyerSdk { "logEvent", {'eventName': eventName, 'eventValues': eventValues}); } + + /// Log ad revenue API. + void logAdRevenue(AdRevenueData adRevenueData) { + _methodChannel.invokeMethod("logAdRevenue", adRevenueData.toMap()); + } + /// Sets the host name and the host prefix. /// This is only relevant if you need to switch between HTTPS environments. void setHost(String hostPrefix, String hostName) { diff --git a/pubspec.yaml b/pubspec.yaml index 6d75bf3f..a060eba4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,11 +1,11 @@ name: appsflyer_sdk description: A Flutter plugin for AppsFlyer SDK. Supports iOS and Android. -version: 6.14.3 +version: 6.15.2 homepage: https://github.com/AppsFlyerSDK/flutter_appsflyer_sdk environment: - sdk: '>=2.12.0 <4.0.0' + sdk: '>=2.17.0 <4.0.0' flutter: ">=1.10.0" dependencies: From e01cc25acbfd70ef5aac8c50221f4014572b932f Mon Sep 17 00:00:00 2001 From: Dani-Koza-AF Date: Wed, 19 Feb 2025 15:45:52 +0200 Subject: [PATCH 15/42] closing potential memory leaks --- .../appsflyer/appsflyersdk/AppsflyerSdkPlugin.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java b/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java index ed342673..72df3fd3 100644 --- a/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java +++ b/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java @@ -72,7 +72,6 @@ public class AppsflyerSdkPlugin implements MethodCallHandler, FlutterPlugin, Act //private FlutterView mFlutterView; private Context mContext; private Application mApplication; - private Intent mIntent; private MethodChannel mMethodChannel; private MethodChannel mCallbackChannel; private Activity activity; @@ -175,7 +174,6 @@ private void onAttachedToEngine(Context applicationContext, BinaryMessenger mess mMethodChannel.setMethodCallHandler(this); mCallbackChannel = new MethodChannel(messenger, AppsFlyerConstants.AF_CALLBACK_CHANNEL); mCallbackChannel.setMethodCallHandler(callbacksHandler); - } @@ -1077,32 +1075,33 @@ public void onDetachedFromEngine(FlutterPluginBinding binding) { mEventChannel.setStreamHandler(null); mEventChannel = null; AppsFlyerPurchaseConnector.INSTANCE.onDetachedFromEngine(binding); - + mContext = null; + mApplication = null; } @Override public void onAttachedToActivity(ActivityPluginBinding binding) { activity = binding.getActivity(); - mIntent = binding.getActivity().getIntent(); mApplication = binding.getActivity().getApplication(); binding.addOnNewIntentListener(onNewIntentListener); } @Override public void onDetachedFromActivityForConfigChanges() { - + this.activity = null; } @Override public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) { sendCachedCallbacksToDart(); binding.addOnNewIntentListener(onNewIntentListener); + activity = binding.getActivity(); } @Override public void onDetachedFromActivity() { activity = null; saveCallbacks = true; + AppsFlyerLib.getInstance().unregisterConversionListener(); } - } From 93a6b03f82e11c9a64533e54b00fa1fb4119da31 Mon Sep 17 00:00:00 2001 From: Dani-Koza-AF Date: Wed, 19 Feb 2025 17:16:17 +0200 Subject: [PATCH 16/42] removed duplicated declarations = --- .gitignore | 5 ---- lib/src/appsflyer_constants.dart | 50 -------------------------------- 2 files changed, 55 deletions(-) diff --git a/.gitignore b/.gitignore index a585496d..ccc54646 100644 --- a/.gitignore +++ b/.gitignore @@ -114,9 +114,4 @@ node_modules/ covBadgeGen.js coverage/ .env -example/windows/* -example/macos/* -example/linux/* -example/web/* -example/analysis_options.yaml diff --git a/lib/src/appsflyer_constants.dart b/lib/src/appsflyer_constants.dart index fef931f6..67ed135d 100644 --- a/lib/src/appsflyer_constants.dart +++ b/lib/src/appsflyer_constants.dart @@ -103,53 +103,3 @@ enum AFMediationNetwork { } } } - -enum AFMediationNetwork { - ironSource, - applovinMax, - googleAdMob, - fyber, - appodeal, - admost, - topon, - tradplus, - yandex, - chartboost, - unity, - toponPte, - customMediation, - directMonetizationNetwork; - - String get value { - switch (this) { - case AFMediationNetwork.ironSource: - return "ironsource"; - case AFMediationNetwork.applovinMax: - return "applovin_max"; - case AFMediationNetwork.googleAdMob: - return "google_admob"; - case AFMediationNetwork.fyber: - return "fyber"; - case AFMediationNetwork.appodeal: - return "appodeal"; - case AFMediationNetwork.admost: - return "admost"; - case AFMediationNetwork.topon: - return "topon"; - case AFMediationNetwork.tradplus: - return "tradplus"; - case AFMediationNetwork.yandex: - return "yandex"; - case AFMediationNetwork.chartboost: - return "chartboost"; - case AFMediationNetwork.unity: - return "unity"; - case AFMediationNetwork.toponPte: - return "topon_pte"; - case AFMediationNetwork.customMediation: - return "custom_mediation"; - case AFMediationNetwork.directMonetizationNetwork: - return "direct_monetization_network"; - } - } -} From fa40e5baa44af14ed4e136240a226b5479c77450 Mon Sep 17 00:00:00 2001 From: Dani-Koza-AF Date: Thu, 20 Feb 2025 13:07:18 +0200 Subject: [PATCH 17/42] updated example project dependencies Aligned Flutter's Android compileOptions to AppsFlyer's Android SDK. --- android/build.gradle | 8 ++++- .../gradle/wrapper/gradle-wrapper.properties | 2 +- example/android/app/build.gradle | 26 +++++++--------- .../android/app/src/main/AndroidManifest.xml | 8 ++--- example/android/build.gradle | 13 -------- .../gradle/wrapper/gradle-wrapper.properties | 4 ++- example/android/settings.gradle | 30 ++++++++++++++----- 7 files changed, 47 insertions(+), 44 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 84a1590a..c4c65867 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -21,7 +21,7 @@ def includeConnector = project.findProperty('appsflyer.enable_purchase_connector android { defaultConfig { minSdkVersion 19 - compileSdk 33 + compileSdk 35 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" multiDexEnabled true @@ -38,6 +38,12 @@ android { } includeConnector ? ['src/main/include-connector'] : ['src/main/exlude-connector'] } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + kotlinOptions { jvmTarget = '17' } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index ffed3a25..e411586a 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index ca22ec0a..4ec36bec 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -1,3 +1,9 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { @@ -6,11 +12,6 @@ if (localPropertiesFile.exists()) { } } -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' @@ -21,21 +22,17 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - android { compileSdkVersion flutter.compileSdkVersion ndkVersion flutter.ndkVersion compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = '17' } sourceSets { @@ -60,12 +57,9 @@ android { signingConfig signingConfigs.debug } } + namespace 'com.appsflyer.appsflyersdkexample' } flutter { source '../..' } - -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" -} diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index f3f57fe9..7bf60143 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - - + - + diff --git a/example/android/build.gradle b/example/android/build.gradle index 713d7f6e..bc157bd1 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,16 +1,3 @@ -buildscript { - ext.kotlin_version = '1.7.10' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.2.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - allprojects { repositories { google() diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index 3c472b99..df97d72b 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/example/android/settings.gradle b/example/android/settings.gradle index 44e62bcf..99a54c6d 100644 --- a/example/android/settings.gradle +++ b/example/android/settings.gradle @@ -1,11 +1,25 @@ -include ':app' +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() -def localPropertiesFile = new File(rootProject.projectDir, "local.properties") -def properties = new Properties() + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") -assert localPropertiesFile.exists() -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version '8.8.1' apply false + id "org.jetbrains.kotlin.android" version "2.0.0" apply false +} + +include ":app" \ No newline at end of file From ac1d0ae7a02aef84128fe04e465236099f563f73 Mon Sep 17 00:00:00 2001 From: Dani Koza <103039399+Dani-Koza-AF@users.noreply.github.com> Date: Thu, 27 Mar 2025 16:04:36 +0200 Subject: [PATCH 18/42] Push notification data collection documentation updates (#381) * updated BasicIntegration.md * docs: updated push notification API's --- doc/API.md | 93 +++++++++++++++++++++++++++++------------ doc/BasicIntegration.md | 8 ++-- 2 files changed, 71 insertions(+), 30 deletions(-) diff --git a/doc/API.md b/doc/API.md index 94cc932b..2041e824 100644 --- a/doc/API.md +++ b/doc/API.md @@ -35,7 +35,6 @@ - [getHostPrefix](#getHostPrefix) - [updateServerUninstallToken](#updateServerUninstallToken) - [Validate Purchase](#validatePurchase) -- [setPushNotification](#setPushNotification)[DEPRECATED] - [sendPushNotificationData](#sendPushNotificationData) - [addPushNotificationDeepLinkPath](#addPushNotificationDeepLinkPath) - [User Invite](#userInvite) @@ -543,48 +542,87 @@ appsflyerSdk.onPurchaseValidation((res){ ``` --- -** `void setPushNotification(bool isEnabled)`[DEPRECATED]** +## ** `void sendPushNotificationData(Map? userInfo)`** -_Example:_ -```dart -appsFlyerSdk.setPushNotification(true); -``` ---- -** `void sendPushNotificationData(Map? userInfo)`** +Push-notification campaigns are used to create re-engagements with existing users -> [Learn more here](https://support.appsflyer.com/hc/en-us/articles/207364076-Measuring-Push-Notification-Re-Engagement-Campaigns) -Push-notification campaigns are used to create fast re-engagements with existing users. +🟩 **Android:**
+The AppsFlyer SDK **requires a** **valid Activity context** to process the push payload. +**Do NOT call this method from the background isolate** (e.g., _firebaseMessagingBackgroundHandler), as the activity is not yet created. +Instead, **delay calling this method** until the Flutter app is fully resumed and the activity is alive. -[Learn more](https://support.appsflyer.com/hc/en-us/articles/207364076-Measuring-Push-Notification-Re-Engagement-Campaigns) +🍎 **iOS:**
+This method can be safely called at any point during app launch or when receiving a push notification. -For Android: AppsFlyer SDK uses the activity in order to process the push payload. Make sure you call this api when the app's activity is available (NOT dead state). -_Example:_ +_**Usage example with Firebase Cloud Messaging:**_
+Given the fact that push message data contains custom key called `af` that contains the attribution data you want to send to AppsFlyer in JSON format. The following attribution parameters are required: `pid`, `is_retargeting`, `c`. + +đŸ“Ļ **Example Push Message Payload** +```json +{ + "af": { + "c": "test_campaign", + "is_retargeting": true, + "pid": "push_provider_int", + }, + "aps": { + "alert": "Get 5000 Coins", + "badge": "37", + "sound": "default" + } +} +``` + +1ī¸âƒŖ Handle Foreground Messages +```dart +FirebaseMessaging.onMessage.listen((RemoteMessage message) { + appsFlyerSdk.sendPushNotificationData(message.data); +}); +``` +2ī¸âƒŖ Handle Notification Taps (App in Background) ```dart -final Map userInfo = { - "af":{ - "c": "test_campaign", - "is_retargeting": true, - "pid": "push_provider_int", - }, - "aps":{ - "alert": "Get 5000 Coins", - "badge": "37", - "sound": "default" - } - }; +FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) { + appsFlyerSdk.sendPushNotificationData(message.data); +}); +``` +3ī¸âƒŖ Handle App Launch from Push (Terminated State) +Store the payload using `_firebaseMessagingBackgroundHandler`, then pass it to AppsFlyer once the app is resumed. +```dart +Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('pending_af_push', jsonEncode(message.data)); +} -appsFlyerSdk.sendPushNotificationData(userInfo); +// In your main() or splash screen after Flutter is initialized: +void handlePendingPush() async { + final prefs = await SharedPreferences.getInstance(); + final json = prefs.getString('pending_af_push'); + if (json != null) { + final payload = jsonDecode(json); + appsFlyerSdk.sendPushNotificationData(payload); + await prefs.remove('pending_af_push'); + } +} ``` +Call handlePendingPush() during app startup (e.g., in your main() or inside your splash screen after ensuring Flutter is initialized). + --- -**
`void addPushNotificationDeepLinkPath(List deeplinkPath)`** +## ** `void addPushNotificationDeepLinkPath(List deeplinkPath)`** + +Registers a **custom key path** for resolving deep links inside **custom JSON payloads** in push notifications. + +This is the recommended method of integrating AppsFlyer with push notifications. [Learn more here.](https://support.appsflyer.com/hc/en-us/articles/207364076-Measuring-Push-Notification-Re-Engagement-Campaigns)
+> âš ī¸ This method must be called BEFORE the AppsFlyer SDK is started — either before calling appsFlyerSdk.initSdk() (if using default auto-start), or before appsFlyerSdk.startSDK() (if using manual start mode). âš ī¸ + _Example:_ ```dart appsFlyerSdk.addPushNotificationDeepLinkPath(["deeply", "nested", "deep_link"]); ``` -This call matches the following payload structure: +With this configuration, the SDK will extract the URL from the following push payload: ```json { @@ -595,6 +633,7 @@ This call matches the following payload structure: } } ``` + --- **
User Invite** diff --git a/doc/BasicIntegration.md b/doc/BasicIntegration.md index eb7e472d..9ac20a2c 100644 --- a/doc/BasicIntegration.md +++ b/doc/BasicIntegration.md @@ -31,11 +31,13 @@ AppsflyerSdk appsflyerSdk = AppsflyerSdk(appsFlyerOptions); | manualStart | bool | Prevents from the SDK from sending the launch request after using appsFlyer.initSdk(...). When using this property, the apps needs to manually trigger the appsFlyer.startSdk() API to report the app launch. | The next step is to call `initSdk` which have the optional boolean parameters `registerConversionDataCallback` and the deeplink callbacks: `registerOnAppOpenAttributionCallback` -`registerOnDeepLinkingCallback` -Please keep in mind that registering the `registerOnDeepLinkingCallback` will override the `registerOnAppOpenAttributionCallback`, as the latter is a Legacy callback used for direct deep-linking, please read more about this in our DeepLinking guide. +`registerOnDeepLinkingCallback`. +> These are **all set to false by default**, meaning listeners will only be registered if you explicitly pass true. -After we call `initSdk` we can use all of AppsFlyer SDK features. +> Please keep in mind that registering the `registerOnDeepLinkingCallback` will override the `registerOnAppOpenAttributionCallback`, as the latter is a Legacy callback used for direct deep-linking, please read more about this in our DeepLinking guide. +After we call `initSdk` we can use all of AppsFlyer SDK features. +Here’s an example of how to register all three: ```dart await appsflyerSdk.initSdk( registerConversionDataCallback: true, From 0f124f937a59086a48e7ef655561d8b14ed63bcf Mon Sep 17 00:00:00 2001 From: Dani-Koza-AF Date: Wed, 9 Apr 2025 16:38:12 +0300 Subject: [PATCH 19/42] reverting addition of PC --- README.md | 1 - android/build.gradle | 31 +- .../AppsFlyerPurchaseConnector.kt | 8 - .../AppsFlyerPurchaseConnector.kt | 207 -------- .../appsflyersdk/ConnectorWrapper.kt | 251 ---------- .../appsflyersdk/AppsFlyerConstants.java | 7 +- .../appsflyersdk/AppsflyerSdkPlugin.java | 2 - doc/PurchaseConnector.md | 323 ------------ example/android/gradle.properties | 1 - example/ios/Podfile | 4 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- ios/Classes/AppsflyerSdkPlugin.m | 8 +- .../PurchaseConnectorPlugin.swift | 170 ------- ios/appsflyer_sdk.podspec | 34 +- lib/appsflyer_sdk.dart | 13 - lib/appsflyer_sdk.g.dart | 461 ------------------ lib/src/appsflyer_constants.dart | 76 --- lib/src/appsflyer_options.dart | 2 +- .../connector_callbacks.dart | 18 - .../missing_configuration_exception.dart | 11 - .../in_app_purchase_validation_result.dart | 33 -- .../purchase_connector/models/ios_error.dart | 15 - .../models/jvm_throwable.dart | 16 - .../models/product_purchase.dart | 44 -- .../models/subscription_purchase.dart | 343 ------------- .../subscription_validation_result.dart | 32 -- .../models/validation_failure_data.dart | 20 - .../purchase_connector.dart | 254 ---------- .../purchase_connector_configuration.dart | 16 - pubspec.yaml | 3 - 30 files changed, 24 insertions(+), 2382 deletions(-) delete mode 100644 android/src/main/exlude-connector/com/appsflyer/appsflyersdk/AppsFlyerPurchaseConnector.kt delete mode 100644 android/src/main/include-connector/com/appsflyer/appsflyersdk/AppsFlyerPurchaseConnector.kt delete mode 100644 android/src/main/include-connector/com/appsflyer/appsflyersdk/ConnectorWrapper.kt delete mode 100644 doc/PurchaseConnector.md delete mode 100644 ios/PurchaseConnector/PurchaseConnectorPlugin.swift delete mode 100644 lib/appsflyer_sdk.g.dart delete mode 100644 lib/src/purchase_connector/connector_callbacks.dart delete mode 100644 lib/src/purchase_connector/missing_configuration_exception.dart delete mode 100644 lib/src/purchase_connector/models/in_app_purchase_validation_result.dart delete mode 100644 lib/src/purchase_connector/models/ios_error.dart delete mode 100644 lib/src/purchase_connector/models/jvm_throwable.dart delete mode 100644 lib/src/purchase_connector/models/product_purchase.dart delete mode 100644 lib/src/purchase_connector/models/subscription_purchase.dart delete mode 100644 lib/src/purchase_connector/models/subscription_validation_result.dart delete mode 100644 lib/src/purchase_connector/models/validation_failure_data.dart delete mode 100644 lib/src/purchase_connector/purchase_connector.dart delete mode 100644 lib/src/purchase_connector/purchase_connector_configuration.dart diff --git a/README.md b/README.md index 0d9980d4..9640498d 100644 --- a/README.md +++ b/README.md @@ -64,5 +64,4 @@ You can read more about it [here](https://dev.appsflyer.com/hc/docs/install-andr - [Advanced APIs](/doc/AdvancedAPI.md) - [Testing the integration](/doc/Testing.md) - [APIs](/doc/API.md) -- [Purchase Connector](/doc/PurchaseConnector.md) - [Sample App](/example) diff --git a/android/build.gradle b/android/build.gradle index c4c65867..0969e87b 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -14,14 +14,10 @@ rootProject.allprojects { } } apply plugin: 'com.android.library' -apply plugin: 'org.jetbrains.kotlin.android' - -def includeConnector = project.findProperty('appsflyer.enable_purchase_connector')?.toBoolean() ?: false - android { defaultConfig { - minSdkVersion 19 - compileSdk 35 + minSdkVersion 16 + compileSdk 31 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" multiDexEnabled true @@ -30,32 +26,11 @@ android { disable 'InvalidPackage' } namespace 'com.appsflyer.appsflyersdk' - - sourceSets { - main { - java.srcDirs = ['src/main/java'] - java.srcDirs += includeConnector ? ['src/main/include-connector'] : ['src/main/exlude-connector'] - } - includeConnector ? ['src/main/include-connector'] : ['src/main/exlude-connector'] - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } - - kotlinOptions { - jvmTarget = '17' - } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.appcompat:appcompat:1.0.0' implementation 'com.appsflyer:af-android-sdk:6.15.2' - implementation 'com.android.installreferrer:installreferrer:2.2' -// implementation 'androidx.core:core-ktx:1.13.1' - if (includeConnector) { - implementation 'com.appsflyer:purchase-connector:2.1.0' - } + implementation 'com.android.installreferrer:installreferrer:2.1' } \ No newline at end of file diff --git a/android/src/main/exlude-connector/com/appsflyer/appsflyersdk/AppsFlyerPurchaseConnector.kt b/android/src/main/exlude-connector/com/appsflyer/appsflyersdk/AppsFlyerPurchaseConnector.kt deleted file mode 100644 index 188033f0..00000000 --- a/android/src/main/exlude-connector/com/appsflyer/appsflyersdk/AppsFlyerPurchaseConnector.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.appsflyer.appsflyersdk - -import io.flutter.embedding.engine.plugins.FlutterPlugin - -object AppsFlyerPurchaseConnector: FlutterPlugin { - override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) = Unit - override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) = Unit -} \ No newline at end of file diff --git a/android/src/main/include-connector/com/appsflyer/appsflyersdk/AppsFlyerPurchaseConnector.kt b/android/src/main/include-connector/com/appsflyer/appsflyersdk/AppsFlyerPurchaseConnector.kt deleted file mode 100644 index a923fe9e..00000000 --- a/android/src/main/include-connector/com/appsflyer/appsflyersdk/AppsFlyerPurchaseConnector.kt +++ /dev/null @@ -1,207 +0,0 @@ -package com.appsflyer.appsflyersdk - -import android.content.Context -import android.os.Handler -import android.os.Looper -import io.flutter.embedding.engine.plugins.FlutterPlugin -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import org.json.JSONObject -import java.lang.ref.WeakReference - - -/** - * A Flutter plugin that establishes a bridge between the Flutter appsflyer SDK and the Native Android Purchase Connector. - * - * This plugin utilizes MethodChannels to communicate between Flutter and native Android, - * passing method calls and event callbacks. - * - * @property methodChannel used to set up the communication channel between Flutter and Android. - * @property contextRef a Weak Reference to the application context when the plugin is first attached. Used to build the Appsflyer's Purchase Connector. - * @property connectorWrapper wraps the Appsflyer's Android purchase client and bridge map conversion methods. Used to perform various operations (configure, start/stop observing transactions). - * @property arsListener an object of [MappedValidationResultListener] that handles SubscriptionPurchaseValidationResultListener responses and failures. Lazily initialized. - * @property viapListener an object of [MappedValidationResultListener] that handles InAppValidationResultListener responses and failures. Lazily initialized. - */ -object AppsFlyerPurchaseConnector : FlutterPlugin, MethodChannel.MethodCallHandler { - private var methodChannel: MethodChannel? = null - private var contextRef: WeakReference? = null - private var connectorWrapper: ConnectorWrapper? = null - private val handler by lazy { Handler(Looper.getMainLooper()) } - - private val arsListener: MappedValidationResultListener by lazy { - object : MappedValidationResultListener { - override fun onFailure(result: String, error: Throwable?) { - val resMap = mapOf("result" to result, "error" to error?.toMap()) - methodChannel?.invokeMethodOnUI( - "SubscriptionPurchaseValidationResultListener:onFailure", - resMap - ) - } - - override fun onResponse(p0: Map?) { - methodChannel?.invokeMethodOnUI( - "SubscriptionPurchaseValidationResultListener:onResponse", - p0 - ) - } - } - } - - private val viapListener: MappedValidationResultListener by lazy { - object : MappedValidationResultListener { - override fun onFailure(result: String, error: Throwable?) { - val resMap = mapOf("result" to result, "error" to error?.toMap()) - methodChannel?.invokeMethodOnUI("InAppValidationResultListener:onFailure", resMap) - } - - override fun onResponse(p0: Map?) { - methodChannel?.invokeMethodOnUI("InAppValidationResultListener:onResponse", p0) - } - } - } - - private fun MethodChannel?.invokeMethodOnUI(method: String, args: Any?) = this?.let { - handler.post { - val data = if (args is Map<*, *>) { - JSONObject(args).toString() - } else { - args - } - it.invokeMethod(method, data) - } - } - - - /** - * Called when the plugin is attached to the Flutter engine. - * - * It sets up the MethodChannel and retains the application context. - * - * @param binding The binding provides access to the binary messenger and application context. - */ - override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { - methodChannel = - MethodChannel( - binding.binaryMessenger, - AppsFlyerConstants.AF_PURCHASE_CONNECTOR_CHANNEL - ).also { - it.setMethodCallHandler(this) - } - contextRef = WeakReference(binding.applicationContext) - } - - /** - * Called when the plugin is detached from the Flutter engine. - * - * @param binding The binding that was provided in [onAttachedToEngine]. - */ - override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) = Unit - - /** - * Handles incoming method calls from Flutter. - * - * It either triggers a connector operation or returns an unimplemented error. - * Supported operations are configuring, starting and stopping observing transactions. - * - * @param call The method call from Flutter. - * @param result The result to be returned to Flutter. - */ - override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { - when (call.method) { - "startObservingTransactions" -> startObservingTransactions(result) - "stopObservingTransactions" -> stopObservingTransactions(result) - "configure" -> configure(call, result) - else -> result.notImplemented() - } - } - - /** - * Configures the purchase connector with the parameters sent from Flutter. - * - * @param call The method call from Flutter. - * @param result The result to be returned to Flutter. - */ - private fun configure(call: MethodCall, result: MethodChannel.Result) { - if (connectorWrapper == null) { - contextRef?.get()?.let { ctx -> - connectorWrapper = ConnectorWrapper( - ctx, call.getBoolean("logSubscriptions"), - call.getBoolean("logInApps"), - call.getBoolean("sandbox"), - arsListener, viapListener - ) - result.success(null) - } ?: run { - result.error("402", "Missing context. Is plugin attached to engine?", null) - } - - } else { - result.error("401", "Connector already configured", null) - } - } - - /** - * Starts observing transactions. - * - * @param result The result to be returned to Flutter. - */ - private fun startObservingTransactions(result: MethodChannel.Result) = - connectorOperation(result) { - it.startObservingTransactions() - } - - /** - * Stops observing transactions. - * - * @param result The result to be returned to Flutter. - */ - private fun stopObservingTransactions(result: MethodChannel.Result) = - connectorOperation(result) { - it.stopObservingTransactions() - } - - /** - * Performs a specified operation on the connector after confirming that the connector has been configured. - * - * @param result The result to be returned to Flutter. - * @param exc The operation to be performed on the connector. - */ - private fun connectorOperation( - result: MethodChannel.Result, - exc: (connectorWrapper: ConnectorWrapper) -> Unit - ) { - if (connectorWrapper != null) { - exc(connectorWrapper!!) - result.success(null) - } else { - result.error("404", "Connector not configured, did you called `configure` first?", null) - } - } - - /** - * Converts a [Throwable] to a Map that can be returned to Flutter. - * - * @return A map representing the [Throwable]. - */ - private fun Throwable.toMap(): Map { - return mapOf( - "type" to this::class.simpleName, - "message" to this.message, - "stacktrace" to this.stackTrace.joinToString(separator = "\n") { it.toString() }, - "cause" to this.cause?.toMap() - ) - } - - /** - * Attempts to get a Boolean argument from the method call. - * - * If unsuccessful, it returns the default value. - * - * @param key The key for the argument. - * @param defValue The default value to be returned if the argument does not exist. - * @return The value of the argument or the default value if the argument does not exist. - */ - private fun MethodCall.getBoolean(key: String, defValue: Boolean = false): Boolean = - runCatching { argument(key)!! }.getOrDefault(defValue) - -} \ No newline at end of file diff --git a/android/src/main/include-connector/com/appsflyer/appsflyersdk/ConnectorWrapper.kt b/android/src/main/include-connector/com/appsflyer/appsflyersdk/ConnectorWrapper.kt deleted file mode 100644 index cc9e2bfc..00000000 --- a/android/src/main/include-connector/com/appsflyer/appsflyersdk/ConnectorWrapper.kt +++ /dev/null @@ -1,251 +0,0 @@ -package com.appsflyer.appsflyersdk - -import android.content.Context -import com.appsflyer.api.PurchaseClient -import com.appsflyer.api.Store -import com.appsflyer.internal.models.* -import com.appsflyer.internal.models.InAppPurchaseValidationResult -import com.appsflyer.internal.models.SubscriptionPurchase -import com.appsflyer.internal.models.SubscriptionValidationResult -import com.appsflyer.internal.models.ValidationFailureData - -interface MappedValidationResultListener : PurchaseClient.ValidationResultListener> - -/** - * A connector class that wraps the Android purchase connector client. - * - * This class uses the Builder pattern to configure the Android purchase connector client. - * It implements the [PurchaseClient] interface required by the appsflyer_sdk and translates - * the various callbacks and responses between the two interfaces. - * - * @property context The application context. - * @property logSubs If true, subscription transactions will be logged. - * @property logInApps If true, in-app purchase transactions will be logged. - * @property sandbox If true, the purchase client will be in sandbox mode. - * @property subsListener The listener for subscription purchase validation results. - * @property inAppListener The listener for in-app purchase validation Result. - */ -class ConnectorWrapper( - context: Context, - logSubs: Boolean, - logInApps: Boolean, - sandbox: Boolean, - subsListener: MappedValidationResultListener, - inAppListener: MappedValidationResultListener, -) : - PurchaseClient { - private val connector = - PurchaseClient.Builder(context, Store.GOOGLE).setSandbox(sandbox).logSubscriptions(logSubs) - .autoLogInApps(logInApps).setSubscriptionValidationResultListener(object : - PurchaseClient.SubscriptionPurchaseValidationResultListener { - override fun onResponse(result: Map?) { - subsListener.onResponse(result?.entries?.associate { (k, v) -> k to v.toJsonMap() }) - } - - override fun onFailure(result: String, error: Throwable?) { - subsListener.onFailure(result, error) - } - }).setInAppValidationResultListener(object : PurchaseClient.InAppPurchaseValidationResultListener{ - override fun onResponse(result: Map?) { - inAppListener.onResponse(result?.entries?.associate { (k, v) -> k to v.toJsonMap() }) - } - - override fun onFailure(result: String, error: Throwable?) { - inAppListener.onFailure(result, error) - } - }) - .build() - - /** - * Starts observing all incoming transactions from the play store. - */ - override fun startObservingTransactions() = connector.startObservingTransactions() - - /** - * Stops observing all incoming transactions from the play store. - */ - override fun stopObservingTransactions() = connector.stopObservingTransactions() - - - /** - * Converts [SubscriptionPurchase] to a Json map, which then is delivered to SDK's method response. - * - * @return A map representing this SubscriptionPurchase. - */ - private fun SubscriptionPurchase.toJsonMap(): Map { - return mapOf( - "acknowledgementState" to acknowledgementState, - "canceledStateContext" to canceledStateContext?.toJsonMap(), - "externalAccountIdentifiers" to externalAccountIdentifiers?.toJsonMap(), - "kind" to kind, - "latestOrderId" to latestOrderId, - "lineItems" to lineItems.map { it.toJsonMap() }, - "linkedPurchaseToken" to linkedPurchaseToken, - "pausedStateContext" to pausedStateContext?.toJsonMap(), - "regionCode" to regionCode, - "startTime" to startTime, - "subscribeWithGoogleInfo" to subscribeWithGoogleInfo?.toJsonMap(), - "subscriptionState" to subscriptionState, - "testPurchase" to testPurchase?.toJsonMap() - ) - } - - private fun CanceledStateContext.toJsonMap(): Map { - return mapOf( - "developerInitiatedCancellation" to developerInitiatedCancellation?.toJsonMap(), - "replacementCancellation" to replacementCancellation?.toJsonMap(), - "systemInitiatedCancellation" to systemInitiatedCancellation?.toJsonMap(), - "userInitiatedCancellation" to userInitiatedCancellation?.toJsonMap() - ) - } - - private fun DeveloperInitiatedCancellation.toJsonMap(): Map { - return mapOf() - } - - private fun ReplacementCancellation.toJsonMap(): Map { - return mapOf() - } - - private fun SystemInitiatedCancellation.toJsonMap(): Map { - return mapOf() - } - - private fun UserInitiatedCancellation.toJsonMap(): Map { - return mapOf( - "cancelSurveyResult" to cancelSurveyResult?.toJsonMap(), - "cancelTime" to cancelTime - ) - } - - private fun CancelSurveyResult.toJsonMap(): Map { - return mapOf( - "reason" to reason, - "reasonUserInput" to reasonUserInput - ) - } - - private fun ExternalAccountIdentifiers.toJsonMap(): Map { - return mapOf( - "externalAccountId" to externalAccountId, - "obfuscatedExternalAccountId" to obfuscatedExternalAccountId, - "obfuscatedExternalProfileId" to obfuscatedExternalProfileId - ) - } - - private fun SubscriptionPurchaseLineItem.toJsonMap(): Map { - return mapOf( - "autoRenewingPlan" to autoRenewingPlan?.toJsonMap(), - "deferredItemReplacement" to deferredItemReplacement?.toJsonMap(), - "expiryTime" to expiryTime, - "offerDetails" to offerDetails?.toJsonMap(), - "prepaidPlan" to prepaidPlan?.toJsonMap(), - "productId" to productId - ) - } - - private fun OfferDetails.toJsonMap(): Map { - return mapOf( - "offerTags" to offerTags, - "basePlanId" to basePlanId, - "offerId" to offerId - ) - } - - private fun AutoRenewingPlan.toJsonMap(): Map { - return mapOf( - "autoRenewEnabled" to autoRenewEnabled, - "priceChangeDetails" to priceChangeDetails?.toJsonMap() - ) - } - - private fun SubscriptionItemPriceChangeDetails.toJsonMap(): Map { - return mapOf( - "expectedNewPriceChargeTime" to expectedNewPriceChargeTime, - "newPrice" to newPrice?.toJsonMap(), - "priceChangeMode" to priceChangeMode, - "priceChangeState" to priceChangeState - ) - } - - private fun Money.toJsonMap(): Map { - return mapOf( - "currencyCode" to currencyCode, - "nanos" to nanos, - "units" to units - ) - } - - private fun DeferredItemReplacement.toJsonMap(): Map { - return mapOf("productId" to productId) - } - - private fun PrepaidPlan.toJsonMap(): Map { - return mapOf("allowExtendAfterTime" to allowExtendAfterTime) - } - - private fun PausedStateContext.toJsonMap(): Map { - return mapOf("autoResumeTime" to autoResumeTime) - } - - private fun SubscribeWithGoogleInfo.toJsonMap(): Map { - return mapOf( - "emailAddress" to emailAddress, - "familyName" to familyName, - "givenName" to givenName, - "profileId" to profileId, - "profileName" to profileName - ) - } - - fun TestPurchase.toJsonMap(): Map { - return mapOf() - } - - private fun ProductPurchase.toJsonMap(): Map { - return mapOf( - "kind" to kind, - "purchaseTimeMillis" to purchaseTimeMillis, - "purchaseState" to purchaseState, - "consumptionState" to consumptionState, - "developerPayload" to developerPayload, - "orderId" to orderId, - "purchaseType" to purchaseType, - "acknowledgementState" to acknowledgementState, - "purchaseToken" to purchaseToken, - "productId" to productId, - "quantity" to quantity, - "obfuscatedExternalAccountId" to obfuscatedExternalAccountId, - "obfuscatedExternalProfileId" to obfuscatedExternalProfileId, - "regionCode" to regionCode - ) - } - - /** - * Converts [InAppPurchaseValidationResult] into a map of objects so that the Object can be passed to Flutter using a method channel - * - * @return A map representing this InAppPurchaseValidationResult. - */ - private fun InAppPurchaseValidationResult.toJsonMap(): Map { - return mapOf( - "success" to success, - "productPurchase" to productPurchase?.toJsonMap(), - "failureData" to failureData?.toJsonMap() - ) - } - - private fun SubscriptionValidationResult.toJsonMap(): Map { - return mapOf( - "success" to success, - "subscriptionPurchase" to subscriptionPurchase?.toJsonMap(), - "failureData" to failureData?.toJsonMap() - ) - } - - private fun ValidationFailureData.toJsonMap(): Map { - return mapOf( - "status" to status, - "description" to description - ) - } -} \ No newline at end of file diff --git a/android/src/main/java/com/appsflyer/appsflyersdk/AppsFlyerConstants.java b/android/src/main/java/com/appsflyer/appsflyersdk/AppsFlyerConstants.java index f96251cb..c8a7847a 100644 --- a/android/src/main/java/com/appsflyer/appsflyersdk/AppsFlyerConstants.java +++ b/android/src/main/java/com/appsflyer/appsflyersdk/AppsFlyerConstants.java @@ -22,10 +22,9 @@ public final class AppsFlyerConstants { final static String AF_UDL_CALLBACK = "onDeepLinking"; final static String DISABLE_ADVERTISING_IDENTIFIER = "disableAdvertisingIdentifier"; - final static String AF_EVENTS_CHANNEL = "af-events"; - final static String AF_METHOD_CHANNEL = "af-api"; - final static String AF_PURCHASE_CONNECTOR_CHANNEL = "af-purchase-connector"; - final static String AF_CALLBACK_CHANNEL = "callbacks"; + final static String AF_EVENTS_CHANNEL = "af-events"; + final static String AF_METHOD_CHANNEL = "af-api"; + final static String AF_CALLBACK_CHANNEL = "callbacks"; final static String AF_BROADCAST_ACTION_NAME = "com.appsflyer.appsflyersdk"; diff --git a/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java b/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java index 72df3fd3..26c0eb6e 100644 --- a/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java +++ b/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java @@ -1065,7 +1065,6 @@ private Map replaceNullValues(Map map) { @Override public void onAttachedToEngine(FlutterPluginBinding binding) { onAttachedToEngine(binding.getApplicationContext(), binding.getBinaryMessenger()); - AppsFlyerPurchaseConnector.INSTANCE.onAttachedToEngine(binding); } @Override @@ -1074,7 +1073,6 @@ public void onDetachedFromEngine(FlutterPluginBinding binding) { mMethodChannel = null; mEventChannel.setStreamHandler(null); mEventChannel = null; - AppsFlyerPurchaseConnector.INSTANCE.onDetachedFromEngine(binding); mContext = null; mApplication = null; } diff --git a/doc/PurchaseConnector.md b/doc/PurchaseConnector.md deleted file mode 100644 index b6cde59a..00000000 --- a/doc/PurchaseConnector.md +++ /dev/null @@ -1,323 +0,0 @@ - -# Flutter Purchase Connector -**At a glance:** Automatically validate and measure revenue from in-app purchases and auto-renewable subscriptions to get the full picture of your customers' life cycles and accurate ROAS measurements. -For more information please check the following pages: -* [ROI360 in-app purchase (IAP) and subscription revenue measurement](https://support.appsflyer.com/hc/en-us/articles/7459048170769-ROI360-in-app-purchase-IAP-and-subscription-revenue-measurement?query=purchase) -* [Android Purchase Connector](https://dev.appsflyer.com/hc/docs/purchase-connector-android) -* [iOS Purchase Connector](https://dev.appsflyer.com/hc/docs/purchase-connector-ios) - -🛠 In order for us to provide optimal support, we would kindly ask you to submit any issues to -support@appsflyer.com - -> *When submitting an issue please specify your AppsFlyer sign-up (account) email , your app ID , production steps, logs, code snippets and any additional relevant information.* - -## Table Of Content - - -* [Important Note](#important-note) -* [Adding The Connector To Your Project](#install-connector) - - [How to Opt-In](#install-connector) - - [What Happens if You Use Dart Files Without Opting In?](#install-connector) -* [Basic Integration Of The Connector](#basic-integration) - - [Create PurchaseConnector Instance](#create-instance) - - [Start Observing Transactions](#start) - - [Stop Observing Transactions](#stop) - - [Log Subscriptions](#log-subscriptions) - - [Log In App Purchases](#log-inapps) -* [Register Validation Results Listeners](#validation-callbacks) - - [Cross-Platform Considerations](#cross-platform-considerations) - - [Android Callback Types](#android-callback-types) - - [Android - Subscription Validation Result Listener](#ars-validation-callbacks) - - [Android In Apps Validation Result Listener](#inapps-validation-callbacks) - - [iOS Combined Validation Result Listener](#ios-callback) -* [Testing the Integration](#testing) - - [Android](#testing-android) - - [iOS](#testing-ios) - - [Dart Usage for Android and iOS](#testing-config) -* [ProGuard Rules for Android](#proguard) -* [Full Code Example](#example) - - - -## âš ī¸ âš ī¸ Important Note âš ī¸ âš ī¸ - -The Purchase Connector feature of the AppsFlyer SDK depends on specific libraries provided by Google and Apple for managing in-app purchases: - -- For Android, it depends on the [Google Play Billing Library](https://developer.android.com/google/play/billing/integrate) (Supported versions: 5.x.x - 6.x.x). -- For iOS, it depends on [StoreKit](https://developer.apple.com/documentation/storekit). (Supported version is StoreKit V1) - -However, these dependencies aren't actively included with the SDK. This means that the responsibility of managing these dependencies and including the necessary libraries in your project falls on you as the consumer of the SDK. - -If you're implementing in-app purchases in your app, you'll need to ensure that the Google Play Billing Library (for Android) or StoreKit (for iOS) are included in your project. You can include these libraries manually in your native code, or you can use a third-party Flutter plugin, such as the [`in_app_purchase`](https://pub.dev/packages/in_app_purchase) plugin. - -Remember to appropriately manage these dependencies when implementing the Purchase Validation feature in your app. Failing to include the necessary libraries might result in failures when attempting to conduct in-app purchases or validate purchases. - -## Adding The Connector To Your Project - -The Purchase Connector feature in AppsFlyer SDK Flutter Plugin is an optional enhancement that you can choose to use based on your requirements. This feature is not included by default and you'll have to opt-in if you wish to use it. - -### How to Opt-In - -To opt-in and include this feature in your app, you need to set specific properties based on your platform: - -For **iOS**, in your Podfile located within the `iOS` folder of your Flutter project, set `$AppsFlyerPurchaseConnector` to `true`. -```ruby -$AppsFlyerPurchaseConnector = true -``` -For **Android**, in your `gradle.properties` file located within the `Android` folder of your Flutter project,, set `appsflyer.enable_purchase_connector` to `true`. -```groovy -appsflyer.enable_purchase_connector=true -``` -Once you set these properties, the Purchase Validation feature will be integrated into your project and you can utilize its functionality in your app. - -### What Happens if You Use Dart Files Without Opting In? - -The Dart files for the Purchase Validation feature are always included in the plugin. If you try to use these Dart APIs without opting into the feature, the APIs will not have effect because the corresponding native code necessary for them to function will not be included in your project. - -In such cases, you'll likely experience errors or exceptions when trying to use functionalities provided by the Purchase Validation feature. To avoid these issues, ensure that you opt-in to the feature if you intend to use any related APIs. - -## Basic Integration Of The Connector -### Create PurchaseConnector Instance -The `PurchaseConnector` requires a configuration object of type `PurchaseConnectorConfiguration` at instantiation time. This configuration object governs how the `PurchaseConnector` behaves in your application. - -To properly set up the configuration object, you must specify certain parameters: - -- `logSubscriptions`: If set to `true`, the connector logs all subscription events. -- `logInApps`: If set to `true`, the connector logs all in-app purchase events. -- `sandbox`: If set to `true`, transactions are tested in a sandbox environment. Be sure to set this to `false` in production. - -Here's an example usage: - -```dart -void main() { - final afPurchaseClient = PurchaseConnector( - config: PurchaseConnectorConfiguration( - logSubscriptions: true, // Enables logging of subscription events - logInApps: true, // Enables logging of in-app purchase events - sandbox: true, // Enables testing in a sandbox environment - ), - ); - - // Continue with your application logic... -} -``` - -**IMPORTANT**: The `PurchaseConnectorConfiguration` is required only the first time you instantiate `PurchaseConnector`. If you attempt to create a `PurchaseConnector` instance and no instance has been initialized yet, you must provide a `PurchaseConnectorConfiguration`. If an instance already exists, the system will ignore the configuration provided and will return the existing instance to enforce the singleton pattern. - -For example: - -```dart -void main() { - // Correct usage: Providing configuration at first instantiation - final purchaseConnector1 = PurchaseConnector( - config: PurchaseConnectorConfiguration( - logSubscriptions: true, - logInApps: true, - sandbox: true, - ), - ); - - // Additional instantiations will ignore the provided configuration - // and will return the previously created instance. - final purchaseConnector2 = PurchaseConnector( - config: PurchaseConnectorConfiguration( - logSubscriptions: false, - logInApps: false, - sandbox: false, - ), - ); - - // purchaseConnector1 and purchaseConnector2 point to the same instance - assert(purchaseConnector1 == purchaseConnector2); -} -``` - -Thus, always ensure that the initial configuration fully suits your requirements, as subsequent changes are not considered. - -Remember to set `sandbox` to `false` before releasing your app to production. If the production purchase event is sent in sandbox mode, your event won't be validated properly by AppsFlyer. -### Start Observing Transactions -Start the SDK instance to observe transactions.
- -**âš ī¸ Please Note** -> This should be called right after calling the `AppsflyerSdk` [start](https://github.com/AppsFlyerSDK/appsflyer-flutter-plugin/blob/master/doc/BasicIntegration.md#startsdk). -> Calling `startObservingTransactions` activates a listener that automatically observes new billing transactions. This includes new and existing subscriptions and new in app purchases. -> The best practice is to activate the listener as early as possible. -```dart - // start - afPurchaseClient.startObservingTransactions(); -``` - -###
Stop Observing Transactions -Stop the SDK instance from observing transactions.
-**âš ī¸ Please Note** -> This should be called if you would like to stop the Connector from listening to billing transactions. This removes the listener and stops observing new transactions. -> An example for using this API is if the app wishes to stop sending data to AppsFlyer due to changes in the user's consent (opt-out from data sharing). Otherwise, there is no reason to call this method. -> If you do decide to use it, it should be called right before calling the Android SDK's [`stop`](https://dev.appsflyer.com/hc/docs/android-sdk-reference-appsflyerlib#stop) API - -```dart - // start - afPurchaseClient.stopObservingTransactions(); -``` - -###
Log Subscriptions -Enables automatic logging of subscription events.
-Set true to enable, false to disable.
-If this field is not used, by default, the connector will not record Subscriptions.
-```dart -final afPurchaseClient = PurchaseConnector( - config: PurchaseConnectorConfiguration(logSubscriptions: true)); -``` - -###
Log In App Purchases -Enables automatic logging of In-App purchase events
-Set true to enable, false to disable.
-If this field is not used, by default, the connector will not record In App Purchases.
- -```dart -final afPurchaseClient = PurchaseConnector( - config: PurchaseConnectorConfiguration(logInApps: true)); -``` - - -##
Register Validation Results Listeners -You can register listeners to get the validation results once getting a response from AppsFlyer servers to let you know if the purchase was validated successfully.
- -###
Cross-Platform Considerations - -The AppsFlyer SDK Flutter plugin acts as a bridge between your Flutter app and the underlying native SDKs provided by AppsFlyer. It's crucial to understand that the native infrastructure of iOS and Android is quite different, and so is the AppsFlyer SDK built on top of them. These differences are reflected in how you would handle callbacks separately for each platform. - -In the iOS environment, there is a single callback method `didReceivePurchaseRevenueValidationInfo` to handle both subscriptions and in-app purchases. You set this callback using `setDidReceivePurchaseRevenueValidationInfo`. - -On the other hand, Android segregates callbacks for subscriptions and in-app purchases. It provides two separate listener methods - `setSubscriptionValidationResultListener` for subscriptions and `setInAppValidationResultListener` for in-app purchases. These listener methods register callback handlers for `OnResponse` (executed when a successful response is received) and `OnFailure` (executed when a failure occurs, including due to a network exception or non-200/OK response from the server). - -By splitting the callbacks, you can ensure platform-specific responses and tailor your app's behavior accordingly. It's crucial to consider these nuances to ensure a smooth integration of AppsFlyer SDK into your Flutter application. - -### Android Callback Types - -| Listener Method | Description | -|-------------------------------|--------------| -| `onResponse(result: Result?)` | Invoked when we got 200 OK response from the server (INVALID purchase is considered to be successful response and will be returned to this callback) | -|`onFailure(result: String, error: Throwable?)`|Invoked when we got some network exception or non 200/OK response from the server.| - -### Android - Subscription Validation Result Listener - -```dart -// set listeners for Android -afPurchaseClient.setSubscriptionValidationResultListener( - (Map? result) { - // handle subscription validation result for Android -}, (String result, JVMThrowable? error) { - // handle subscription validation error for Android -}); -``` - -### Android In Apps Validation Result Listener -```dart -afPurchaseClient.setInAppValidationResultListener( - (Map? result) { - // handle in-app validation result for Android - }, (String result, JVMThrowable? error) { - // handle in-app validation error for Android -}); -``` - -### iOS Combined Validation Result Listener -```dart -afPurchaseClient.setDidReceivePurchaseRevenueValidationInfo((validationInfo, error) { - // handle subscription and in-app validation result and errors for iOS -}); -``` - - -## Testing the Integration - -With the AppsFlyer SDK, you can select which environment will be used for validation - either **production** or **sandbox**. By default, the environment is set to production. However, while testing your app, you should use the sandbox environment. - -### Android - -For Android, testing your integration with the [Google Play Billing Library](https://developer.android.com/google/play/billing/test) should use the sandbox environment. - -To set the environment to sandbox in Flutter, just set the `sandbox` parameter in the `PurchaseConnectorConfiguration` to `true` when instantiating `PurchaseConnector`. - -Remember to switch the environment back to production (set `sandbox` to `false`) before uploading your app to the Google Play Store. - -### iOS - -To test purchases in an iOS environment on a real device with a TestFlight sandbox account, you also need to set `sandbox` to `true`. - -> *IMPORTANT NOTE: Before releasing your app to production please be sure to set `sandbox` to `false`. If a production purchase event is sent in sandbox mode, your event will not be validated properly! * - -### Dart Usage for Android and iOS - -For both Android and iOS, you can set the sandbox environment using the `sandbox` parameter in the `PurchaseConnectorConfiguration` when you instantiate `PurchaseConnector` in your Dart code like this: - -```dart -// Testing in a sandbox environment -final purchaseConnector = PurchaseConnector( - PurchaseConnectorConfiguration(sandbox: true) -); -``` - -Remember to set `sandbox` back to `false` before releasing your app to production. If the production purchase event is sent in sandbox mode, your event won't be validated properly. - -## ProGuard Rules for Android - -If you are using ProGuard to obfuscate your APK for Android, you need to ensure that it doesn't interfere with the functionality of AppsFlyer SDK and its Purchase Connector feature. - -Add following keep rules to your `proguard-rules.pro` file: - -```groovy --keep class com.appsflyer.** { *; } --keep class kotlin.jvm.internal.Intrinsics{ *; } --keep class kotlin.collections.**{ *; } -``` - -## Full Code Example -```dart -PurchaseConnectorConfiguration config = PurchaseConnectorConfiguration( - logSubscriptions: true, logInApps: true, sandbox: false); -final afPurchaseClient = PurchaseConnector(config: config); - -// set listeners for Android -afPurchaseClient.setSubscriptionValidationResultListener( - (Map? result) { - // handle subscription validation result for Android - result?.entries.forEach((element) { - debugPrint( - "Subscription Validation Result\n\t Token: ${element.key}\n\tresult: ${jsonEncode(element.value.toJson())}"); - }); -}, (String result, JVMThrowable? error) { - // handle subscription validation error for Android - var errMsg = error != null ? jsonEncode(error.toJson()) : null; - debugPrint( - "Subscription Validation Result\n\t result: $result\n\terror: $errMsg"); -}); - -afPurchaseClient.setInAppValidationResultListener( - (Map? result) { - // handle in-app validation result for Android - result?.entries.forEach((element) { - debugPrint( - "In App Validation Result\n\t Token: ${element.key}\n\tresult: ${jsonEncode(element.value.toJson())}"); - }); -}, (String result, JVMThrowable? error) { - // handle in-app validation error for Android - var errMsg = error != null ? jsonEncode(error.toJson()) : null; - debugPrint( - "In App Validation Result\n\t result: $result\n\terror: $errMsg"); -}); - -// set listener for iOS -afPurchaseClient - .setDidReceivePurchaseRevenueValidationInfo((validationInfo, error) { - var validationInfoMsg = - validationInfo != null ? jsonEncode(validationInfo) : null; - var errMsg = error != null ? jsonEncode(error.toJson()) : null; - debugPrint( - "iOS Validation Result\n\t validationInfo: $validationInfoMsg\n\terror: $errMsg"); - // handle subscription and in-app validation result and errors for iOS -}); - -// start -afPurchaseClient.startObservingTransactions(); -``` \ No newline at end of file diff --git a/example/android/gradle.properties b/example/android/gradle.properties index 2e2c8994..94adc3a3 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -1,4 +1,3 @@ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true -appsflyer.enable_purchase_connector=true \ No newline at end of file diff --git a/example/ios/Podfile b/example/ios/Podfile index 280bf5f3..a72f9d72 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -3,7 +3,7 @@ # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' -$AppsFlyerPurchaseConnector = true + project 'Runner', { 'Debug' => :debug, 'Profile' => :release, @@ -30,7 +30,7 @@ flutter_ios_podfile_setup target 'Runner' do use_frameworks! use_modular_headers! - # Include appsflyer_sdk/PurchaseConnector as a pod dependency + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) target 'RunnerTests' do inherit! :search_paths diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 8e3ca5df..87131a09 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ -#ifdef ENABLE_PURCHASE_CONNECTOR -#import "appsflyer_sdk/appsflyer_sdk-Swift.h" -#endif + typedef void (*bypassDidFinishLaunchingWithOption)(id, SEL, NSInteger); typedef void (*bypassDisableAdvertisingIdentifier)(id, SEL, BOOL); typedef void (*bypassWaitForATTUserAuthorization)(id, SEL, NSTimeInterval); @@ -58,9 +56,6 @@ - (instancetype)initWithMessenger:(nonnull NSObject *)me } + (void)registerWithRegistrar:(NSObject*)registrar { -#ifdef ENABLE_PURCHASE_CONNECTOR - [PurchaseConnectorPlugin registerWithRegistrar:registrar]; -#endif id messenger = [registrar messenger]; FlutterMethodChannel *channel = [FlutterMethodChannel methodChannelWithName:afMethodChannel binaryMessenger:messenger]; FlutterMethodChannel *callbackChannel = [FlutterMethodChannel methodChannelWithName:afCallbacksMethodChannel binaryMessenger:messenger]; @@ -200,6 +195,7 @@ - (void)setConsentData:(FlutterMethodCall*)call result:(FlutterResult)result { BOOL isUserSubjectToGDPR = [consentDict[@"isUserSubjectToGDPR"] boolValue]; BOOL hasConsentForDataUsage = [consentDict[@"hasConsentForDataUsage"] boolValue]; BOOL hasConsentForAdsPersonalization = [consentDict[@"hasConsentForAdsPersonalization"] boolValue]; + AppsFlyerConsent *consentData; if(isUserSubjectToGDPR){ consentData = [[AppsFlyerConsent alloc] initForGDPRUserWithHasConsentForDataUsage:hasConsentForDataUsage diff --git a/ios/PurchaseConnector/PurchaseConnectorPlugin.swift b/ios/PurchaseConnector/PurchaseConnectorPlugin.swift deleted file mode 100644 index dfd60f41..00000000 --- a/ios/PurchaseConnector/PurchaseConnectorPlugin.swift +++ /dev/null @@ -1,170 +0,0 @@ -// -// PurchaseConnectorPlugin.swift -// appsflyer_sdk -// -// Created by Paz Lavi on 11/06/2024. -// -import Foundation -import PurchaseConnector -import Flutter - -/// `PurchaseConnectorPlugin` is a `FlutterPlugin` implementation that acts as the bridge between Flutter and the PurchaseConnector iOS SDK. -/// This class is responsible for processing incoming method calls from the Dart layer (via a MethodChannel) and translating these calls to the appropriate tasks in the PurchaseConnector SDK. -@objc public class PurchaseConnectorPlugin : NSObject, FlutterPlugin { - - /// Methods channel name constant to be used across plugin. - private static let AF_PURCHASE_CONNECTOR_CHANNEL = "af-purchase-connector" - - /// Singleton instance of `PurchaseConnectorPlugin` ensures this plugin acts as a centralized point of contact for all method calls. - internal static let shared = PurchaseConnectorPlugin() - - /// An instance of `PurchaseConnector`. - /// This will be intentionally set to `nil` by default and will be initialized once we call the `configure` method via Flutter. - private var connector: PurchaseConnector? = nil - - /// Instance of method channel providing a bridge to Dart code. - private var methodChannel: FlutterMethodChannel? = nil - - private var logOptions: AutoLogPurchaseRevenueOptions = [] - - /// Constants used in method channel for Flutter calls. - private let logSubscriptionsKey = "logSubscriptions" - private let logInAppsKey = "logInApps" - private let sandboxKey = "sandbox" - - /// Private constructor, used to prevent direct instantiation of this class and ensure singleton behaviour. - private override init() {} - - /// Mandatory method needed to register the plugin with iOS part of Flutter app. - public static func register(with registrar: FlutterPluginRegistrar) { - /// Create a new method channel with the registrar. - shared.methodChannel = FlutterMethodChannel(name: AF_PURCHASE_CONNECTOR_CHANNEL, binaryMessenger: registrar.messenger()) - shared.methodChannel!.setMethodCallHandler(shared.methodCallHandler) - - } - - /// Method called when a Flutter method call occurs. It handles and routes flutter method invocations. - private func methodCallHandler(call: FlutterMethodCall, result: @escaping FlutterResult) { - switch(call.method) { - /// Match incoming flutter calls from Dart side to its corresponding native method. - case "configure": - configure(call: call, result: result) - case "startObservingTransactions": - startObservingTransactions(result: result) - case "stopObservingTransactions": - stopObservingTransactions(result: result) - default: - /// This condition handles an error scenario where the method call doesn't match any predefined cases. - result(FlutterMethodNotImplemented) - } - } - - /// This method corresponds to the 'configure' call from Flutter and initiates the PurchaseConnector instance. - private func configure(call: FlutterMethodCall, result: @escaping FlutterResult) { - /// Perform a guard check to ensure that we do not reconfigure an existing connector. - guard connector == nil else { - result(FlutterError(code: "401", message: "Connector already configured", details: nil)) - return - } - - /// Obtain a shared instance of PurchaseConnector - connector = PurchaseConnector.shared() - - /// Extract all the required parameters from Flutter arguments - let arguments = call.arguments as? [String: Any] - let logSubscriptions = arguments?[logSubscriptionsKey] as? Bool ?? false - let logInApps = arguments?[logInAppsKey] as? Bool ?? false - let sandbox = arguments?[sandboxKey] as? Bool ?? false - - /// Define an options variable to manage enabled options. - var options: AutoLogPurchaseRevenueOptions = [] - - /// Based on the arguments, insert the corresponding options. - if logSubscriptions { - options.insert(.autoRenewableSubscriptions) - } - if logInApps { - options.insert(.inAppPurchases) - } - - /// Update the PurchaseConnector instance with these options. - connector!.autoLogPurchaseRevenue = options - logOptions = options - connector!.isSandbox = sandbox - connector!.purchaseRevenueDelegate = self - - /// Report a successful operation back to Dart. - result(nil) - } - - /// This function starts the process of observing transactions in the iOS App Store. - private func startObservingTransactions(result: @escaping FlutterResult) { - connectorOperation(result: result) { connector in - // From the docs: If you called stopObservingTransactions API, set the autoLogPurchaseRevenue value before you call startObservingTransactions next time. - connector.autoLogPurchaseRevenue = self.logOptions - connector.startObservingTransactions() - - } - } - - /// This function stops the process of observing transactions in the iOS App Store. - private func stopObservingTransactions(result: @escaping FlutterResult) { - connectorOperation(result: result) { connector in - connector.stopObservingTransactions() - } - } - - /// Helper function used to extract common logic for operations on the connector. - private func connectorOperation(result: @escaping FlutterResult, operation: @escaping ((PurchaseConnector) -> Void)) { - guard connector != nil else { - result(FlutterError(code: "404", message: "Connector not configured, did you called `configure` first?", details: nil)) - return - } - /// Perform the operation with the current connector - operation(connector!) - - result(nil) - } -} - -/// Extension enabling `PurchaseConnectorPlugin` to conform to `PurchaseRevenueDelegate` -extension PurchaseConnectorPlugin: PurchaseRevenueDelegate { - /// Implementation of the `didReceivePurchaseRevenueValidationInfo` delegate method. - /// When the validation info comes back after a purchase, it is reported back to the Flutter via the method channel. - public func didReceivePurchaseRevenueValidationInfo(_ validationInfo: [AnyHashable : Any]?, error: Error?) { - var resMap: [AnyHashable : Any?] = [ - "validationInfo": validationInfo, - "error" : error?.asDictionary - ] - DispatchQueue.main.async { - self.methodChannel?.invokeMethod("didReceivePurchaseRevenueValidationInfo", arguments: resMap.toJSONString()) - } - } -} - -/// Extending `Error` to have a dictionary representation function. `asDictionary` will turn the current error instance into a dictionary containing `localizedDescription`, `domain` and `code` properties. -extension Error { - var asDictionary: [String: Any] { - var errorMap: [String: Any] = ["localizedDescription": self.localizedDescription] - if let nsError = self as? NSError { - errorMap["domain"] = nsError.domain - errorMap["code"] = nsError.code - } - return errorMap - } -} - -extension Dictionary { - - var jsonData: Data? { - return try? JSONSerialization.data(withJSONObject: self, options: [.prettyPrinted]) - } - - func toJSONString() -> String? { - if let jsonData = jsonData { - let jsonString = String(data: jsonData, encoding: .utf8) - return jsonString - } - return nil - } -} diff --git a/ios/appsflyer_sdk.podspec b/ios/appsflyer_sdk.podspec index c48c2b81..c0305457 100644 --- a/ios/appsflyer_sdk.podspec +++ b/ios/appsflyer_sdk.podspec @@ -1,35 +1,25 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# Pod::Spec.new do |s| s.name = 'appsflyer_sdk' s.version = '6.15.3' s.summary = 'AppsFlyer Integration for Flutter' - s.description = 'AppsFlyer is the market leader in mobile advertising attribution & analytics, helping marketers to pinpoint their targeting, optimize their ad spend and boost their ROI.' + s.description = <<-DESC +AppsFlyer is the market leader in mobile advertising attribution & analytics, helping marketers to pinpoint their targeting, optimize their ad spend and boost their ROI. + DESC s.homepage = 'https://github.com/AppsFlyerSDK/flutter_appsflyer_sdk' s.license = { :type => 'MIT', :file => '../LICENSE' } s.author = { "Appsflyer" => "build@appsflyer.com" } s.source = { :git => "https://github.com/AppsFlyerSDK/flutter_appsflyer_sdk.git", :tag => s.version.to_s } - + + s.ios.deployment_target = '12.0' s.requires_arc = true s.static_framework = true - if defined?($AppsFlyerPurchaseConnector) - s.default_subspecs = 'Core', 'PurchaseConnector' # add this line - else - s.default_subspecs = 'Core' # add this line - end - - s.subspec 'Core' do |ss| - ss.source_files = 'Classes/**/*' - ss.public_header_files = 'Classes/**/*.h' - ss.dependency 'Flutter' - ss.ios.dependency 'AppsFlyerFramework','6.15.3' - end - - s.subspec 'PurchaseConnector' do |ss| - ss.dependency 'Flutter' - ss.ios.dependency 'PurchaseConnector', '6.15.3' - ss.source_files = 'PurchaseConnector/**/*' - ss.public_header_files = 'PurchaseConnector/**/*.h' - ss.pod_target_xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) ENABLE_PURCHASE_CONNECTOR=1' } - end + s.source_files = 'Classes/**/*' + s.public_header_files = 'Classes/**/*.h' + s.dependency 'Flutter' + s.ios.dependency 'AppsFlyerFramework','6.15.3' end diff --git a/lib/appsflyer_sdk.dart b/lib/appsflyer_sdk.dart index ff9df5ce..1d8ab749 100644 --- a/lib/appsflyer_sdk.dart +++ b/lib/appsflyer_sdk.dart @@ -7,7 +7,6 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:json_annotation/json_annotation.dart'; import 'src/callbacks.dart'; @@ -17,18 +16,6 @@ part 'src/appsflyer_options.dart'; part 'src/appsflyer_sdk.dart'; part 'src/udl/deep_link_result.dart'; part 'src/udl/deeplink.dart'; -part 'src/purchase_connector/purchase_connector.dart'; -part 'src/purchase_connector/connector_callbacks.dart'; -part 'src/purchase_connector/missing_configuration_exception.dart'; -part 'src/purchase_connector/purchase_connector_configuration.dart'; -part 'src/purchase_connector/models/subscription_purchase.dart'; -part 'src/purchase_connector/models/in_app_purchase_validation_result.dart'; -part 'src/purchase_connector/models/product_purchase.dart'; -part 'src/purchase_connector/models/subscription_validation_result.dart'; -part 'src/purchase_connector/models/validation_failure_data.dart'; -part 'src/purchase_connector/models/jvm_throwable.dart'; -part 'src/purchase_connector/models/ios_error.dart'; part 'src/appsflyer_consent.dart'; part 'src/appsflyer_request_listener.dart'; -part 'appsflyer_sdk.g.dart'; part 'src/appsflyer_ad_revenue_data.dart'; diff --git a/lib/appsflyer_sdk.g.dart b/lib/appsflyer_sdk.g.dart deleted file mode 100644 index 65275103..00000000 --- a/lib/appsflyer_sdk.g.dart +++ /dev/null @@ -1,461 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'appsflyer_sdk.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -SubscriptionPurchase _$SubscriptionPurchaseFromJson( - Map json) => - SubscriptionPurchase( - json['acknowledgementState'] as String, - json['canceledStateContext'] == null - ? null - : CanceledStateContext.fromJson( - json['canceledStateContext'] as Map), - json['externalAccountIdentifiers'] == null - ? null - : ExternalAccountIdentifiers.fromJson( - json['externalAccountIdentifiers'] as Map), - json['kind'] as String, - json['latestOrderId'] as String, - (json['lineItems'] as List) - .map((e) => - SubscriptionPurchaseLineItem.fromJson(e as Map)) - .toList(), - json['linkedPurchaseToken'] as String?, - json['pausedStateContext'] == null - ? null - : PausedStateContext.fromJson( - json['pausedStateContext'] as Map), - json['regionCode'] as String, - json['startTime'] as String, - json['subscribeWithGoogleInfo'] == null - ? null - : SubscribeWithGoogleInfo.fromJson( - json['subscribeWithGoogleInfo'] as Map), - json['subscriptionState'] as String, - json['testPurchase'] == null - ? null - : TestPurchase.fromJson(json['testPurchase'] as Map), - ); - -Map _$SubscriptionPurchaseToJson( - SubscriptionPurchase instance) => - { - 'acknowledgementState': instance.acknowledgementState, - 'canceledStateContext': instance.canceledStateContext, - 'externalAccountIdentifiers': instance.externalAccountIdentifiers, - 'kind': instance.kind, - 'latestOrderId': instance.latestOrderId, - 'lineItems': instance.lineItems, - 'linkedPurchaseToken': instance.linkedPurchaseToken, - 'pausedStateContext': instance.pausedStateContext, - 'regionCode': instance.regionCode, - 'startTime': instance.startTime, - 'subscribeWithGoogleInfo': instance.subscribeWithGoogleInfo, - 'subscriptionState': instance.subscriptionState, - 'testPurchase': instance.testPurchase, - }; - -CanceledStateContext _$CanceledStateContextFromJson( - Map json) => - CanceledStateContext( - json['developerInitiatedCancellation'] == null - ? null - : DeveloperInitiatedCancellation.fromJson( - json['developerInitiatedCancellation'] as Map), - json['replacementCancellation'] == null - ? null - : ReplacementCancellation.fromJson( - json['replacementCancellation'] as Map), - json['systemInitiatedCancellation'] == null - ? null - : SystemInitiatedCancellation.fromJson( - json['systemInitiatedCancellation'] as Map), - json['userInitiatedCancellation'] == null - ? null - : UserInitiatedCancellation.fromJson( - json['userInitiatedCancellation'] as Map), - ); - -Map _$CanceledStateContextToJson( - CanceledStateContext instance) => - { - 'developerInitiatedCancellation': instance.developerInitiatedCancellation, - 'replacementCancellation': instance.replacementCancellation, - 'systemInitiatedCancellation': instance.systemInitiatedCancellation, - 'userInitiatedCancellation': instance.userInitiatedCancellation, - }; - -DeveloperInitiatedCancellation _$DeveloperInitiatedCancellationFromJson( - Map json) => - DeveloperInitiatedCancellation(); - -Map _$DeveloperInitiatedCancellationToJson( - DeveloperInitiatedCancellation instance) => - {}; - -ReplacementCancellation _$ReplacementCancellationFromJson( - Map json) => - ReplacementCancellation(); - -Map _$ReplacementCancellationToJson( - ReplacementCancellation instance) => - {}; - -SystemInitiatedCancellation _$SystemInitiatedCancellationFromJson( - Map json) => - SystemInitiatedCancellation(); - -Map _$SystemInitiatedCancellationToJson( - SystemInitiatedCancellation instance) => - {}; - -UserInitiatedCancellation _$UserInitiatedCancellationFromJson( - Map json) => - UserInitiatedCancellation( - json['cancelSurveyResult'] == null - ? null - : CancelSurveyResult.fromJson( - json['cancelSurveyResult'] as Map), - json['cancelTime'] as String, - ); - -Map _$UserInitiatedCancellationToJson( - UserInitiatedCancellation instance) => - { - 'cancelSurveyResult': instance.cancelSurveyResult, - 'cancelTime': instance.cancelTime, - }; - -CancelSurveyResult _$CancelSurveyResultFromJson(Map json) => - CancelSurveyResult( - json['reason'] as String, - json['reasonUserInput'] as String, - ); - -Map _$CancelSurveyResultToJson(CancelSurveyResult instance) => - { - 'reason': instance.reason, - 'reasonUserInput': instance.reasonUserInput, - }; - -ExternalAccountIdentifiers _$ExternalAccountIdentifiersFromJson( - Map json) => - ExternalAccountIdentifiers( - json['externalAccountId'] as String, - json['obfuscatedExternalAccountId'] as String, - json['obfuscatedExternalProfileId'] as String, - ); - -Map _$ExternalAccountIdentifiersToJson( - ExternalAccountIdentifiers instance) => - { - 'externalAccountId': instance.externalAccountId, - 'obfuscatedExternalAccountId': instance.obfuscatedExternalAccountId, - 'obfuscatedExternalProfileId': instance.obfuscatedExternalProfileId, - }; - -SubscriptionPurchaseLineItem _$SubscriptionPurchaseLineItemFromJson( - Map json) => - SubscriptionPurchaseLineItem( - json['autoRenewingPlan'] == null - ? null - : AutoRenewingPlan.fromJson( - json['autoRenewingPlan'] as Map), - json['deferredItemReplacement'] == null - ? null - : DeferredItemReplacement.fromJson( - json['deferredItemReplacement'] as Map), - json['expiryTime'] as String, - json['offerDetails'] == null - ? null - : OfferDetails.fromJson(json['offerDetails'] as Map), - json['prepaidPlan'] == null - ? null - : PrepaidPlan.fromJson(json['prepaidPlan'] as Map), - json['productId'] as String, - ); - -Map _$SubscriptionPurchaseLineItemToJson( - SubscriptionPurchaseLineItem instance) => - { - 'autoRenewingPlan': instance.autoRenewingPlan, - 'deferredItemReplacement': instance.deferredItemReplacement, - 'expiryTime': instance.expiryTime, - 'offerDetails': instance.offerDetails, - 'prepaidPlan': instance.prepaidPlan, - 'productId': instance.productId, - }; - -OfferDetails _$OfferDetailsFromJson(Map json) => OfferDetails( - (json['offerTags'] as List?)?.map((e) => e as String).toList(), - json['basePlanId'] as String, - json['offerId'] as String?, - ); - -Map _$OfferDetailsToJson(OfferDetails instance) => - { - 'offerTags': instance.offerTags, - 'basePlanId': instance.basePlanId, - 'offerId': instance.offerId, - }; - -AutoRenewingPlan _$AutoRenewingPlanFromJson(Map json) => - AutoRenewingPlan( - json['autoRenewEnabled'] as bool?, - json['priceChangeDetails'] == null - ? null - : SubscriptionItemPriceChangeDetails.fromJson( - json['priceChangeDetails'] as Map), - ); - -Map _$AutoRenewingPlanToJson(AutoRenewingPlan instance) => - { - 'autoRenewEnabled': instance.autoRenewEnabled, - 'priceChangeDetails': instance.priceChangeDetails, - }; - -SubscriptionItemPriceChangeDetails _$SubscriptionItemPriceChangeDetailsFromJson( - Map json) => - SubscriptionItemPriceChangeDetails( - json['expectedNewPriceChargeTime'] as String, - json['newPrice'] == null - ? null - : Money.fromJson(json['newPrice'] as Map), - json['priceChangeMode'] as String, - json['priceChangeState'] as String, - ); - -Map _$SubscriptionItemPriceChangeDetailsToJson( - SubscriptionItemPriceChangeDetails instance) => - { - 'expectedNewPriceChargeTime': instance.expectedNewPriceChargeTime, - 'newPrice': instance.newPrice, - 'priceChangeMode': instance.priceChangeMode, - 'priceChangeState': instance.priceChangeState, - }; - -Money _$MoneyFromJson(Map json) => Money( - json['currencyCode'] as String, - (json['nanos'] as num).toInt(), - (json['units'] as num).toInt(), - ); - -Map _$MoneyToJson(Money instance) => { - 'currencyCode': instance.currencyCode, - 'nanos': instance.nanos, - 'units': instance.units, - }; - -DeferredItemReplacement _$DeferredItemReplacementFromJson( - Map json) => - DeferredItemReplacement( - json['productId'] as String, - ); - -Map _$DeferredItemReplacementToJson( - DeferredItemReplacement instance) => - { - 'productId': instance.productId, - }; - -PrepaidPlan _$PrepaidPlanFromJson(Map json) => PrepaidPlan( - json['allowExtendAfterTime'] as String?, - ); - -Map _$PrepaidPlanToJson(PrepaidPlan instance) => - { - 'allowExtendAfterTime': instance.allowExtendAfterTime, - }; - -PausedStateContext _$PausedStateContextFromJson(Map json) => - PausedStateContext( - json['autoResumeTime'] as String, - ); - -Map _$PausedStateContextToJson(PausedStateContext instance) => - { - 'autoResumeTime': instance.autoResumeTime, - }; - -SubscribeWithGoogleInfo _$SubscribeWithGoogleInfoFromJson( - Map json) => - SubscribeWithGoogleInfo( - json['emailAddress'] as String, - json['familyName'] as String, - json['givenName'] as String, - json['profileId'] as String, - json['profileName'] as String, - ); - -Map _$SubscribeWithGoogleInfoToJson( - SubscribeWithGoogleInfo instance) => - { - 'emailAddress': instance.emailAddress, - 'familyName': instance.familyName, - 'givenName': instance.givenName, - 'profileId': instance.profileId, - 'profileName': instance.profileName, - }; - -TestPurchase _$TestPurchaseFromJson(Map json) => - TestPurchase(); - -Map _$TestPurchaseToJson(TestPurchase instance) => - {}; - -InAppPurchaseValidationResult _$InAppPurchaseValidationResultFromJson( - Map json) => - InAppPurchaseValidationResult( - json['success'] as bool, - json['productPurchase'] == null - ? null - : ProductPurchase.fromJson( - json['productPurchase'] as Map), - json['failureData'] == null - ? null - : ValidationFailureData.fromJson( - json['failureData'] as Map), - ); - -Map _$InAppPurchaseValidationResultToJson( - InAppPurchaseValidationResult instance) => - { - 'success': instance.success, - 'productPurchase': instance.productPurchase, - 'failureData': instance.failureData, - }; - -InAppPurchaseValidationResultMap _$InAppPurchaseValidationResultMapFromJson( - Map json) => - InAppPurchaseValidationResultMap( - (json['result'] as Map).map( - (k, e) => MapEntry(k, - InAppPurchaseValidationResult.fromJson(e as Map)), - ), - ); - -Map _$InAppPurchaseValidationResultMapToJson( - InAppPurchaseValidationResultMap instance) => - { - 'result': instance.result, - }; - -ProductPurchase _$ProductPurchaseFromJson(Map json) => - ProductPurchase( - json['kind'] as String, - json['purchaseTimeMillis'] as String, - (json['purchaseState'] as num).toInt(), - (json['consumptionState'] as num).toInt(), - json['developerPayload'] as String, - json['orderId'] as String, - (json['purchaseType'] as num).toInt(), - (json['acknowledgementState'] as num).toInt(), - json['purchaseToken'] as String, - json['productId'] as String, - (json['quantity'] as num).toInt(), - json['obfuscatedExternalAccountId'] as String, - json['obfuscatedExternalProfileId'] as String, - json['regionCode'] as String, - ); - -Map _$ProductPurchaseToJson(ProductPurchase instance) => - { - 'kind': instance.kind, - 'purchaseTimeMillis': instance.purchaseTimeMillis, - 'purchaseState': instance.purchaseState, - 'consumptionState': instance.consumptionState, - 'developerPayload': instance.developerPayload, - 'orderId': instance.orderId, - 'purchaseType': instance.purchaseType, - 'acknowledgementState': instance.acknowledgementState, - 'purchaseToken': instance.purchaseToken, - 'productId': instance.productId, - 'quantity': instance.quantity, - 'obfuscatedExternalAccountId': instance.obfuscatedExternalAccountId, - 'obfuscatedExternalProfileId': instance.obfuscatedExternalProfileId, - 'regionCode': instance.regionCode, - }; - -SubscriptionValidationResult _$SubscriptionValidationResultFromJson( - Map json) => - SubscriptionValidationResult( - json['success'] as bool, - json['subscriptionPurchase'] == null - ? null - : SubscriptionPurchase.fromJson( - json['subscriptionPurchase'] as Map), - json['failureData'] == null - ? null - : ValidationFailureData.fromJson( - json['failureData'] as Map), - ); - -Map _$SubscriptionValidationResultToJson( - SubscriptionValidationResult instance) => - { - 'success': instance.success, - 'subscriptionPurchase': instance.subscriptionPurchase, - 'failureData': instance.failureData, - }; - -SubscriptionValidationResultMap _$SubscriptionValidationResultMapFromJson( - Map json) => - SubscriptionValidationResultMap( - (json['result'] as Map).map( - (k, e) => MapEntry(k, - SubscriptionValidationResult.fromJson(e as Map)), - ), - ); - -Map _$SubscriptionValidationResultMapToJson( - SubscriptionValidationResultMap instance) => - { - 'result': instance.result, - }; - -ValidationFailureData _$ValidationFailureDataFromJson( - Map json) => - ValidationFailureData( - (json['status'] as num).toInt(), - json['description'] as String, - ); - -Map _$ValidationFailureDataToJson( - ValidationFailureData instance) => - { - 'status': instance.status, - 'description': instance.description, - }; - -JVMThrowable _$JVMThrowableFromJson(Map json) => JVMThrowable( - json['type'] as String, - json['message'] as String, - json['stacktrace'] as String, - json['cause'] == null - ? null - : JVMThrowable.fromJson(json['cause'] as Map), - ); - -Map _$JVMThrowableToJson(JVMThrowable instance) => - { - 'type': instance.type, - 'message': instance.message, - 'stacktrace': instance.stacktrace, - 'cause': instance.cause, - }; - -IosError _$IosErrorFromJson(Map json) => IosError( - json['localizedDescription'] as String, - json['domain'] as String, - (json['code'] as num).toInt(), - ); - -Map _$IosErrorToJson(IosError instance) => { - 'localizedDescription': instance.localizedDescription, - 'domain': instance.domain, - 'code': instance.code, - }; diff --git a/lib/src/appsflyer_constants.dart b/lib/src/appsflyer_constants.dart index 67ed135d..9eefe3ce 100644 --- a/lib/src/appsflyer_constants.dart +++ b/lib/src/appsflyer_constants.dart @@ -3,10 +3,6 @@ part of appsflyer_sdk; enum EmailCryptType { EmailCryptTypeNone, EmailCryptTypeSHA256 } class AppsflyerConstants { - - static const String RE_CONFIGURE_ERROR_MSG = "[PurchaseConnector] Re configure instance is not permitted. Returned the existing instance"; - static const String MISSING_CONFIGURATION_EXCEPTION_MSG = "Could not create an instance without configuration"; - static const String AF_DEV_KEY = "afDevKey"; static const String AF_APP_Id = "afAppId"; static const String AF_IS_DEBUG = "isDebug"; @@ -23,7 +19,6 @@ class AppsflyerConstants { static const String AF_EVENTS_CHANNEL = "af-events"; static const String AF_METHOD_CHANNEL = "af-api"; - static const String AF_PURCHASE_CONNECTOR_CHANNEL = "af-purchase-connector"; static const String AF_VALIDATE_PURCHASE = "validatePurchase"; static const String APP_INVITE_ONE_LINK = "appInviteOneLink"; @@ -31,75 +26,4 @@ class AppsflyerConstants { static const String DISABLE_COLLECT_ASA = "disableCollectASA"; static const String DISABLE_ADVERTISING_IDENTIFIER = "disableAdvertisingIdentifier"; - - // Adding method constants - static const String SUBSCRIPTION_PURCHASE_VALIDATION_RESULT_LISTENER_ON_RESPONSE = - "SubscriptionPurchaseValidationResultListener:onResponse"; - static const String SUBSCRIPTION_PURCHASE_VALIDATION_RESULT_LISTENER_ON_FAILURE = - "SubscriptionPurchaseValidationResultListener:onFailure"; - static const String IN_APP_VALIDATION_RESULT_LISTENER_ON_RESPONSE = - "InAppValidationResultListener:onResponse"; - static const String IN_APP_VALIDATION_RESULT_LISTENER_ON_FAILURE = - "InAppValidationResultListener:onFailure"; - static const String DID_RECEIVE_PURCHASE_REVENUE_VALIDATION_INFO = - "didReceivePurchaseRevenueValidationInfo"; - -// Adding key constants - static const String RESULT = "result"; - static const String ERROR = "error"; - static const String VALIDATION_INFO = "validationInfo"; - static const String CONFIGURE_KEY = "configure"; - static const String LOG_SUBS_KEY = "logSubscriptions"; - static const String LOG_IN_APP_KEY = "logInApps"; - static const String SANDBOX_KEY = "sandbox"; -} - -enum AFMediationNetwork { - ironSource, - applovinMax, - googleAdMob, - fyber, - appodeal, - admost, - topon, - tradplus, - yandex, - chartboost, - unity, - toponPte, - customMediation, - directMonetizationNetwork; - - String get value { - switch (this) { - case AFMediationNetwork.ironSource: - return "ironsource"; - case AFMediationNetwork.applovinMax: - return "applovin_max"; - case AFMediationNetwork.googleAdMob: - return "google_admob"; - case AFMediationNetwork.fyber: - return "fyber"; - case AFMediationNetwork.appodeal: - return "appodeal"; - case AFMediationNetwork.admost: - return "admost"; - case AFMediationNetwork.topon: - return "topon"; - case AFMediationNetwork.tradplus: - return "tradplus"; - case AFMediationNetwork.yandex: - return "yandex"; - case AFMediationNetwork.chartboost: - return "chartboost"; - case AFMediationNetwork.unity: - return "unity"; - case AFMediationNetwork.toponPte: - return "topon_pte"; - case AFMediationNetwork.customMediation: - return "custom_mediation"; - case AFMediationNetwork.directMonetizationNetwork: - return "direct_monetization_network"; - } - } } diff --git a/lib/src/appsflyer_options.dart b/lib/src/appsflyer_options.dart index 240f5556..2e6c66b5 100644 --- a/lib/src/appsflyer_options.dart +++ b/lib/src/appsflyer_options.dart @@ -15,7 +15,7 @@ class AppsFlyerOptions { /// Requires [afDevKey] and [appId] as mandatory Named parameters. /// All other parameters are optional, it's allows greater flexibility /// when invoking the constructor. - /// When [manualStart] is true the startSDK method must be called + /// When manual start is true the startSDK must be called AppsFlyerOptions({ required this.afDevKey, this.showDebug = false, diff --git a/lib/src/purchase_connector/connector_callbacks.dart b/lib/src/purchase_connector/connector_callbacks.dart deleted file mode 100644 index c908cfea..00000000 --- a/lib/src/purchase_connector/connector_callbacks.dart +++ /dev/null @@ -1,18 +0,0 @@ -part of appsflyer_sdk; - -/// Type definition for a general purpose listener. -typedef PurchaseConnectorListener = Function(dynamic); -/// Type definition for a listener which gets called when the `PurchaseConnectorImpl` receives purchase revenue validation info for iOS. -typedef DidReceivePurchaseRevenueValidationInfo = Function( - Map? validationInfo, IosError? error); -/// Invoked when a 200 OK response is received from the server. -/// Note: An INVALID purchase is considered to be a successful response and will also be returned by this callback. -/// -/// [result] Server's response. -typedef OnResponse = Function(Map? result); - -/// Invoked when a network exception occurs or a non 200/OK response is received from the server. -/// -/// [result] The server's response. -/// [error] The exception that occurred during execution. -typedef OnFailure = Function(String result, JVMThrowable? error); diff --git a/lib/src/purchase_connector/missing_configuration_exception.dart b/lib/src/purchase_connector/missing_configuration_exception.dart deleted file mode 100644 index eae23676..00000000 --- a/lib/src/purchase_connector/missing_configuration_exception.dart +++ /dev/null @@ -1,11 +0,0 @@ -part of appsflyer_sdk; -/// Exception thrown when the configuration is missing. -class MissingConfigurationException implements Exception { - final String message; - - MissingConfigurationException( - {this.message = AppsflyerConstants.MISSING_CONFIGURATION_EXCEPTION_MSG}); - - @override - String toString() => 'ConfigurationException: $message'; -} diff --git a/lib/src/purchase_connector/models/in_app_purchase_validation_result.dart b/lib/src/purchase_connector/models/in_app_purchase_validation_result.dart deleted file mode 100644 index 0846a180..00000000 --- a/lib/src/purchase_connector/models/in_app_purchase_validation_result.dart +++ /dev/null @@ -1,33 +0,0 @@ -part of appsflyer_sdk; - -@JsonSerializable() -class InAppPurchaseValidationResult { - - bool success; - ProductPurchase? productPurchase; - ValidationFailureData? failureData; - - InAppPurchaseValidationResult( - this.success, - this.productPurchase, - this.failureData - ); - - - - factory InAppPurchaseValidationResult.fromJson(Map json) => _$InAppPurchaseValidationResultFromJson(json); - - Map toJson() => _$InAppPurchaseValidationResultToJson(this); - -} - -@JsonSerializable() -class InAppPurchaseValidationResultMap{ - Map result; - - InAppPurchaseValidationResultMap(this.result); - factory InAppPurchaseValidationResultMap.fromJson(Map json) => _$InAppPurchaseValidationResultMapFromJson(json); - - Map toJson() => _$InAppPurchaseValidationResultMapToJson(this); - -} diff --git a/lib/src/purchase_connector/models/ios_error.dart b/lib/src/purchase_connector/models/ios_error.dart deleted file mode 100644 index aa671066..00000000 --- a/lib/src/purchase_connector/models/ios_error.dart +++ /dev/null @@ -1,15 +0,0 @@ -part of appsflyer_sdk; - -@JsonSerializable() -class IosError{ - String localizedDescription; - String domain; - int code; - - - IosError(this.localizedDescription, this.domain, this.code); - - factory IosError.fromJson(Map json) => _$IosErrorFromJson(json); - - Map toJson() => _$IosErrorToJson(this); -} \ No newline at end of file diff --git a/lib/src/purchase_connector/models/jvm_throwable.dart b/lib/src/purchase_connector/models/jvm_throwable.dart deleted file mode 100644 index 04fb07e5..00000000 --- a/lib/src/purchase_connector/models/jvm_throwable.dart +++ /dev/null @@ -1,16 +0,0 @@ -part of appsflyer_sdk; - -@JsonSerializable() -class JVMThrowable{ - String type; - String message; - String stacktrace; - JVMThrowable? cause; - - JVMThrowable(this.type, this.message, this.stacktrace, this.cause); - - factory JVMThrowable.fromJson(Map json) => _$JVMThrowableFromJson(json); - - Map toJson() => _$JVMThrowableToJson(this); - -} \ No newline at end of file diff --git a/lib/src/purchase_connector/models/product_purchase.dart b/lib/src/purchase_connector/models/product_purchase.dart deleted file mode 100644 index e6e3ceb6..00000000 --- a/lib/src/purchase_connector/models/product_purchase.dart +++ /dev/null @@ -1,44 +0,0 @@ -part of appsflyer_sdk; - -@JsonSerializable() -class ProductPurchase { - - String kind; - String purchaseTimeMillis; - int purchaseState; - int consumptionState; - String developerPayload; - String orderId; - int purchaseType; - int acknowledgementState; - String purchaseToken; - String productId; - int quantity; - String obfuscatedExternalAccountId; - String obfuscatedExternalProfileId; - String regionCode; - - ProductPurchase( - this.kind, - this.purchaseTimeMillis, - this.purchaseState, - this.consumptionState, - this.developerPayload, - this.orderId, - this.purchaseType, - this.acknowledgementState, - this.purchaseToken, - this.productId, - this.quantity, - this.obfuscatedExternalAccountId, - this.obfuscatedExternalProfileId, - this.regionCode - ); - - - - factory ProductPurchase.fromJson(Map json) => _$ProductPurchaseFromJson(json); - - Map toJson() => _$ProductPurchaseToJson(this); - -} \ No newline at end of file diff --git a/lib/src/purchase_connector/models/subscription_purchase.dart b/lib/src/purchase_connector/models/subscription_purchase.dart deleted file mode 100644 index 55ede1e6..00000000 --- a/lib/src/purchase_connector/models/subscription_purchase.dart +++ /dev/null @@ -1,343 +0,0 @@ -part of appsflyer_sdk; - -@JsonSerializable() -class SubscriptionPurchase { - - String acknowledgementState; - CanceledStateContext? canceledStateContext; - ExternalAccountIdentifiers? externalAccountIdentifiers; - String kind; - String latestOrderId; - List lineItems; - String? linkedPurchaseToken; - PausedStateContext? pausedStateContext; - String regionCode; - String startTime; - SubscribeWithGoogleInfo? subscribeWithGoogleInfo; - String subscriptionState; - TestPurchase? testPurchase; - - SubscriptionPurchase( - this.acknowledgementState, - this.canceledStateContext, - this.externalAccountIdentifiers, - this.kind, - this.latestOrderId, - this.lineItems, - this.linkedPurchaseToken, - this.pausedStateContext, - this.regionCode, - this.startTime, - this.subscribeWithGoogleInfo, - this.subscriptionState, - this.testPurchase - ); - - - - factory SubscriptionPurchase.fromJson(Map json) => _$SubscriptionPurchaseFromJson(json); - - Map toJson() => _$SubscriptionPurchaseToJson(this); - -} - - -@JsonSerializable() -class CanceledStateContext { - - DeveloperInitiatedCancellation? developerInitiatedCancellation; - ReplacementCancellation? replacementCancellation; - SystemInitiatedCancellation? systemInitiatedCancellation; - UserInitiatedCancellation? userInitiatedCancellation; - - CanceledStateContext( - this.developerInitiatedCancellation, - this.replacementCancellation, - this.systemInitiatedCancellation, - this.userInitiatedCancellation - ); - - - - factory CanceledStateContext.fromJson(Map json) => _$CanceledStateContextFromJson(json); - - Map toJson() => _$CanceledStateContextToJson(this); - -} - -@JsonSerializable() -class DeveloperInitiatedCancellation{ - DeveloperInitiatedCancellation(); - factory DeveloperInitiatedCancellation.fromJson(Map json) => _$DeveloperInitiatedCancellationFromJson(json); - - Map toJson() => _$DeveloperInitiatedCancellationToJson(this); -} - -@JsonSerializable() -class ReplacementCancellation{ - ReplacementCancellation(); - factory ReplacementCancellation.fromJson(Map json) => _$ReplacementCancellationFromJson(json); - - Map toJson() => _$ReplacementCancellationToJson(this); -} - -@JsonSerializable() -class SystemInitiatedCancellation{ - SystemInitiatedCancellation(); - factory SystemInitiatedCancellation.fromJson(Map json) => _$SystemInitiatedCancellationFromJson(json); - - Map toJson() => _$SystemInitiatedCancellationToJson(this); -} - - -@JsonSerializable() -class UserInitiatedCancellation { - - CancelSurveyResult? cancelSurveyResult; - String cancelTime; - - UserInitiatedCancellation( - this.cancelSurveyResult, - this.cancelTime - ); - - - - factory UserInitiatedCancellation.fromJson(Map json) => _$UserInitiatedCancellationFromJson(json); - - Map toJson() => _$UserInitiatedCancellationToJson(this); - -} - -@JsonSerializable() -class CancelSurveyResult { - - String reason; - String reasonUserInput; - - CancelSurveyResult( - this.reason, - this.reasonUserInput - ); - - - - factory CancelSurveyResult.fromJson(Map json) => _$CancelSurveyResultFromJson(json); - - Map toJson() => _$CancelSurveyResultToJson(this); - -} - -@JsonSerializable() -class ExternalAccountIdentifiers { - - String externalAccountId; - String obfuscatedExternalAccountId; - String obfuscatedExternalProfileId; - - ExternalAccountIdentifiers( - this.externalAccountId, - this.obfuscatedExternalAccountId, - this.obfuscatedExternalProfileId - ); - - - - factory ExternalAccountIdentifiers.fromJson(Map json) => _$ExternalAccountIdentifiersFromJson(json); - - Map toJson() => _$ExternalAccountIdentifiersToJson(this); - -} - -@JsonSerializable() -class SubscriptionPurchaseLineItem { - - AutoRenewingPlan? autoRenewingPlan; - DeferredItemReplacement? deferredItemReplacement; - String expiryTime; - OfferDetails? offerDetails; - PrepaidPlan? prepaidPlan; - String productId; - - SubscriptionPurchaseLineItem( - this.autoRenewingPlan, - this.deferredItemReplacement, - this.expiryTime, - this.offerDetails, - this.prepaidPlan, - this.productId - ); - - - - factory SubscriptionPurchaseLineItem.fromJson(Map json) => _$SubscriptionPurchaseLineItemFromJson(json); - - Map toJson() => _$SubscriptionPurchaseLineItemToJson(this); - -} - -@JsonSerializable() -class OfferDetails { - - List? offerTags; - String basePlanId; - String? offerId; - - OfferDetails( - this.offerTags, - this.basePlanId, - this.offerId - ); - - - - factory OfferDetails.fromJson(Map json) => _$OfferDetailsFromJson(json); - - Map toJson() => _$OfferDetailsToJson(this); - -} - -@JsonSerializable() -class AutoRenewingPlan { - - bool? autoRenewEnabled; - SubscriptionItemPriceChangeDetails? priceChangeDetails; - - AutoRenewingPlan( - this.autoRenewEnabled, - this.priceChangeDetails - ); - - - - factory AutoRenewingPlan.fromJson(Map json) => _$AutoRenewingPlanFromJson(json); - - Map toJson() => _$AutoRenewingPlanToJson(this); - -} - -@JsonSerializable() -class SubscriptionItemPriceChangeDetails { - - String expectedNewPriceChargeTime; - Money? newPrice; - String priceChangeMode; - String priceChangeState; - - SubscriptionItemPriceChangeDetails( - this.expectedNewPriceChargeTime, - this.newPrice, - this.priceChangeMode, - this.priceChangeState - ); - - - - factory SubscriptionItemPriceChangeDetails.fromJson(Map json) => _$SubscriptionItemPriceChangeDetailsFromJson(json); - - Map toJson() => _$SubscriptionItemPriceChangeDetailsToJson(this); - -} - -@JsonSerializable() -class Money { - - String currencyCode; - int nanos; - int units; - - Money( - this.currencyCode, - this.nanos, - this.units - ); - - - - factory Money.fromJson(Map json) => _$MoneyFromJson(json); - - Map toJson() => _$MoneyToJson(this); - -} -@JsonSerializable() -class DeferredItemReplacement { - - String productId; - - DeferredItemReplacement( - this.productId - ); - - - - factory DeferredItemReplacement.fromJson(Map json) => _$DeferredItemReplacementFromJson(json); - - Map toJson() => _$DeferredItemReplacementToJson(this); - -} - -@JsonSerializable() -class PrepaidPlan { - - String? allowExtendAfterTime; - - PrepaidPlan( - this.allowExtendAfterTime - ); - - - - factory PrepaidPlan.fromJson(Map json) => _$PrepaidPlanFromJson(json); - - Map toJson() => _$PrepaidPlanToJson(this); - -} - -@JsonSerializable() -class PausedStateContext { - - String autoResumeTime; - - PausedStateContext( - this.autoResumeTime - ); - - - - factory PausedStateContext.fromJson(Map json) => _$PausedStateContextFromJson(json); - - Map toJson() => _$PausedStateContextToJson(this); - -} -@JsonSerializable() -class SubscribeWithGoogleInfo { - - String emailAddress; - String familyName; - String givenName; - String profileId; - String profileName; - - SubscribeWithGoogleInfo( - this.emailAddress, - this.familyName, - this.givenName, - this.profileId, - this.profileName - ); - - - - factory SubscribeWithGoogleInfo.fromJson(Map json) => _$SubscribeWithGoogleInfoFromJson(json); - - Map toJson() => _$SubscribeWithGoogleInfoToJson(this); - -} - -@JsonSerializable() -class TestPurchase{ - TestPurchase(); - factory TestPurchase.fromJson(Map json) => _$TestPurchaseFromJson(json); - - Map toJson() => _$TestPurchaseToJson(this); -} \ No newline at end of file diff --git a/lib/src/purchase_connector/models/subscription_validation_result.dart b/lib/src/purchase_connector/models/subscription_validation_result.dart deleted file mode 100644 index b9294176..00000000 --- a/lib/src/purchase_connector/models/subscription_validation_result.dart +++ /dev/null @@ -1,32 +0,0 @@ -part of appsflyer_sdk; - -@JsonSerializable() -class SubscriptionValidationResult { - - bool success; - SubscriptionPurchase? subscriptionPurchase; - ValidationFailureData? failureData; - - SubscriptionValidationResult( - this.success, - this.subscriptionPurchase, - this.failureData - ); - - - - factory SubscriptionValidationResult.fromJson(Map json) => _$SubscriptionValidationResultFromJson(json); - - Map toJson() => _$SubscriptionValidationResultToJson(this); - -} - -@JsonSerializable() -class SubscriptionValidationResultMap{ - Map result; - - SubscriptionValidationResultMap(this.result); - factory SubscriptionValidationResultMap.fromJson(Map json) => _$SubscriptionValidationResultMapFromJson(json); - - Map toJson() => _$SubscriptionValidationResultMapToJson(this); -} diff --git a/lib/src/purchase_connector/models/validation_failure_data.dart b/lib/src/purchase_connector/models/validation_failure_data.dart deleted file mode 100644 index 57b3d137..00000000 --- a/lib/src/purchase_connector/models/validation_failure_data.dart +++ /dev/null @@ -1,20 +0,0 @@ -part of appsflyer_sdk; - -@JsonSerializable() -class ValidationFailureData { - - int status; - String description; - - ValidationFailureData( - this.status, - this.description - ); - - - - factory ValidationFailureData.fromJson(Map json) => _$ValidationFailureDataFromJson(json); - - Map toJson() => _$ValidationFailureDataToJson(this); - -} \ No newline at end of file diff --git a/lib/src/purchase_connector/purchase_connector.dart b/lib/src/purchase_connector/purchase_connector.dart deleted file mode 100644 index e962823e..00000000 --- a/lib/src/purchase_connector/purchase_connector.dart +++ /dev/null @@ -1,254 +0,0 @@ -part of appsflyer_sdk; - -/// Interface representing a purchase connector. -abstract class PurchaseConnector { - /// Starts observing transactions. - void startObservingTransactions(); - - /// Stops observing transactions. - void stopObservingTransactions(); - - /// Sets the listener for Android subscription validation results. - /// - /// [onResponse] Function to be executed when a successful response is received. - /// [onFailure] Function to be executed when a failure occurs (network exception or non 200/OK response from the server). - void setSubscriptionValidationResultListener( - OnResponse? onResponse, - OnFailure? onFailure); - - /// Sets the listener for Android in-app validation results. - /// - /// [onResponse] Function to be executed when a successful response is received. - /// [onFailure] Function to be executed when a failure occurs (network exception or non 200/OK response from the server). - - void setInAppValidationResultListener( - OnResponse? onResponse, - OnFailure? onFailure); - - /// Sets the listener for iOS subscription and in-app validation results. - /// Parameter: - /// [callback] the function to be executed when `DidReceivePurchaseRevenueValidationInfo` is called. - void setDidReceivePurchaseRevenueValidationInfo( - DidReceivePurchaseRevenueValidationInfo? callback); - - /// Creates a new PurchaseConnector instance. - /// Parameter: - /// [config] the configuration to be used when creating a new `PurchaseConnector` instance. - factory PurchaseConnector({PurchaseConnectorConfiguration? config}) => - _PurchaseConnectorImpl(config: config); -} - -/// The implementation of the PurchaseConnector. -/// -/// This class is responsible for establishing a connection with Appsflyer purchase connector, -/// starting/stopping observing transactions, setting listeners for various validation results. -class _PurchaseConnectorImpl implements PurchaseConnector { - /// Singleton instance of the PurchaseConnectorImpl. - static _PurchaseConnectorImpl? _instance; - - /// Method channel to communicate with the Appsflyer Purchase Connector. - final MethodChannel _methodChannel; - - /// Response handler for SubscriptionValidationResult (Android). - OnResponse? _arsOnResponse; - - /// Failure handler for SubscriptionValidationResult (Android). - OnFailure? _arsOnFailure; - - /// Response handler for InAppPurchaseValidationResult (Android). - OnResponse? _viapOnResponse; - - /// Failure handler for InAppPurchaseValidationResult (Android). - OnFailure? _viapOnFailure; - - /// Callback handler for receiving validation info for iOS. - DidReceivePurchaseRevenueValidationInfo? - _didReceivePurchaseRevenueValidationInfo; - - /// Internal constructor. Initializes the instance and sets up method call handler. - _PurchaseConnectorImpl._internal( - this._methodChannel, PurchaseConnectorConfiguration config) { - _methodChannel.setMethodCallHandler(_methodCallHandler); - _methodChannel.invokeMethod(AppsflyerConstants.CONFIGURE_KEY, { - AppsflyerConstants.LOG_SUBS_KEY: config.logSubscriptions, - AppsflyerConstants.LOG_IN_APP_KEY: config.logInApps, - AppsflyerConstants.SANDBOX_KEY: config.sandbox, - }); - } - - /// Factory constructor. - /// - /// This factory ensures that only a single instance of `PurchaseConnectorImpl` is used throughout the program - /// by implementing the Singleton design pattern. If an instance already exists, it's returned. - /// - /// The [config] parameter is optional and is used only when creating the first instance of `PurchaseConnectorImpl`. - /// Once an instance is created, the same instance will be returned in subsequent calls, and the [config] - /// parameter will be ignored. Thus, it's valid to call this factory without a config if an instance already exists. - /// - /// If there is no existing instance and the [config] is not provided, a `MissingConfigurationException` will be thrown. - factory _PurchaseConnectorImpl({PurchaseConnectorConfiguration? config}) { - if (_instance == null && config == null) { - // no instance exist and config not provided. We Can't create instance without config - throw MissingConfigurationException(); - } else if (_instance == null && config != null) { - // no existing instance. Create new instance and apply config - MethodChannel methodChannel = - const MethodChannel(AppsflyerConstants.AF_PURCHASE_CONNECTOR_CHANNEL); - _instance = _PurchaseConnectorImpl._internal(methodChannel, config); - } else if (_instance != null && config != null) { - debugPrint(AppsflyerConstants.RE_CONFIGURE_ERROR_MSG); - } - return _instance!; - } - - /// Starts observing the transactions. - @override - void startObservingTransactions() { - _methodChannel.invokeMethod("startObservingTransactions"); - } - - /// Stops observing the transactions. - @override - void stopObservingTransactions() { - _methodChannel.invokeMethod("stopObservingTransactions"); - } - - /// Sets the function to be executed when iOS validation info is received. - @override - void setDidReceivePurchaseRevenueValidationInfo( - DidReceivePurchaseRevenueValidationInfo? callback) { - _didReceivePurchaseRevenueValidationInfo = callback; - } - - /// Sets the listener for Android in-app validation results. - /// - /// [onResponse] Function to be executed when a successful response is received. - /// [onFailure] Function to be executed when a failure occurs (network exception or non 200/OK response from the server). - @override - void setInAppValidationResultListener( - OnResponse? onResponse, - OnFailure? onFailure) { - _viapOnResponse = onResponse; - _viapOnFailure = onFailure; - } - - /// Sets the listener for Android subscription validation results. - /// - /// [onResponse] Function to be executed when a successful response is received. - /// [onFailure] Function to be executed when a failure occurs (network exception or non 200/OK response from the server). - @override - void setSubscriptionValidationResultListener( - OnResponse? onResponse, - OnFailure? onFailure) { - _arsOnResponse = onResponse; - _arsOnFailure = onFailure; - } - - /// Method call handler for different operations. Called by the _methodChannel. - Future _methodCallHandler(MethodCall call) async { - dynamic callMap = jsonDecode(call.arguments); - switch (call.method) { - case AppsflyerConstants - .SUBSCRIPTION_PURCHASE_VALIDATION_RESULT_LISTENER_ON_RESPONSE: - _handleSubscriptionPurchaseValidationResultListenerOnResponse(callMap); - break; - case AppsflyerConstants - .SUBSCRIPTION_PURCHASE_VALIDATION_RESULT_LISTENER_ON_FAILURE: - _handleSubscriptionPurchaseValidationResultListenerOnFailure(callMap); - break; - case AppsflyerConstants.IN_APP_VALIDATION_RESULT_LISTENER_ON_RESPONSE: - _handleInAppValidationResultListenerOnResponse(callMap); - break; - case AppsflyerConstants.IN_APP_VALIDATION_RESULT_LISTENER_ON_FAILURE: - _handleInAppValidationResultListenerOnFailure(callMap); - break; - case AppsflyerConstants.DID_RECEIVE_PURCHASE_REVENUE_VALIDATION_INFO: - _handleDidReceivePurchaseRevenueValidationInfo(callMap); - break; - default: - throw ArgumentError("Method not found."); - } - } - - /// Handles response for the subscription purchase validation result listener. - /// - /// [callbackData] is the callback data expected in the form of a map. - void _handleSubscriptionPurchaseValidationResultListenerOnResponse( - dynamic callbackData) { - _handleValidationResultListenerOnResponse( - {"result": callbackData}, - _arsOnResponse, - (value) => SubscriptionValidationResultMap.fromJson(value).result, - ); - } - - /// Handles response for the in-app validation result listener. - /// - /// [callbackData] is the callback data expected in the form of a map. - void _handleInAppValidationResultListenerOnResponse( - dynamic callbackData) { - _handleValidationResultListenerOnResponse( - {"result": callbackData}, - _viapOnResponse, - (value) => InAppPurchaseValidationResultMap.fromJson(value).result, - ); - } - - /// Handles failure for the subscription purchase validation result listener. - /// - /// [callbackData] is the callback data expected in the form of a map. - void _handleSubscriptionPurchaseValidationResultListenerOnFailure( - Map callbackData) { - _handleValidationResultListenerOnFailure(callbackData, _arsOnFailure); - } - - /// Handles failure for the in-app validation result listener. - /// - /// [callbackData] is the callback data expected in the form of a map. - void _handleInAppValidationResultListenerOnFailure(dynamic callbackData) { - _handleValidationResultListenerOnFailure(callbackData, _viapOnFailure); - } - - /// Handles the reception of purchase revenue validation info. - /// - /// [callbackData] is the callback data expected in the form of a map. - void _handleDidReceivePurchaseRevenueValidationInfo(dynamic callbackData) { - var validationInfo = callbackData[AppsflyerConstants.VALIDATION_INFO] - as Map?; - var errorMap = - callbackData[AppsflyerConstants.ERROR] as Map?; - var error = errorMap != null ? IosError.fromJson(errorMap) : null; - if (_didReceivePurchaseRevenueValidationInfo != null) { - _didReceivePurchaseRevenueValidationInfo!(validationInfo, error); - } - } - - /// Handles the response for a validation result listener. - /// - /// [callbackData] is the callback data expected in the form of a map. - /// [onResponse] is a function to be called upon response. - /// [converter] is a function used for converting `[callbackData]` to result type `T` - void _handleValidationResultListenerOnResponse(dynamic callbackData, - OnResponse? onResponse, Map? Function(dynamic) converter) { - Map? res = converter(callbackData); - if (onResponse != null) { - onResponse(res); - } else { - } - } - - /// Handles failure for a validation result listener. - /// - /// [callbackData] is the callback data expected in the form of a map. - /// [onFailureCallback] is a function to be called on failure. - void _handleValidationResultListenerOnFailure( - dynamic callbackData, OnFailure? onFailureCallback) { - var resultMsg = callbackData[AppsflyerConstants.RESULT] as String; - var errorMap = - callbackData[AppsflyerConstants.ERROR] as Map?; - var error = errorMap != null ? JVMThrowable.fromJson(errorMap) : null; - if (onFailureCallback != null) { - onFailureCallback(resultMsg, error); - } - } -} diff --git a/lib/src/purchase_connector/purchase_connector_configuration.dart b/lib/src/purchase_connector/purchase_connector_configuration.dart deleted file mode 100644 index 3832dc7d..00000000 --- a/lib/src/purchase_connector/purchase_connector_configuration.dart +++ /dev/null @@ -1,16 +0,0 @@ -part of appsflyer_sdk; - -/// Contains the configuration settings for a `PurchaseConnector`. -/// -/// This class controls automatic logging of In-App purchase and subscription events. -/// It also allows setting a sandbox environment for validation. -class PurchaseConnectorConfiguration { - bool logSubscriptions; - bool logInApps; - bool sandbox; - - PurchaseConnectorConfiguration( - {this.logSubscriptions = false, - this.logInApps = false, - this.sandbox = false}); -} diff --git a/pubspec.yaml b/pubspec.yaml index a060eba4..cb6c3695 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,6 @@ environment: dependencies: flutter: sdk: flutter - json_annotation: ^4.9.0 dev_dependencies: flutter_test: @@ -19,8 +18,6 @@ dev_dependencies: test: ^1.16.5 mockito: ^5.4.4 effective_dart: ^1.3.0 - build_runner: ^2.3.0 - json_serializable: ^6.5.4 flutter: From f14a8b94780dd6f03d97a641cf1a2c9ff6d5b8e0 Mon Sep 17 00:00:00 2001 From: Dani-Koza-AF Date: Thu, 10 Apr 2025 09:17:11 +0300 Subject: [PATCH 20/42] post revert fixes --- .gitignore | 2 + android/build.gradle | 14 ++++- .../appsflyersdk/AppsflyerSdkPlugin.java | 1 + example/android/app/build.gradle | 2 +- lib/src/appsflyer_options.dart | 52 ++++++++++++++++++- 5 files changed, 67 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index ccc54646..2f5397af 100644 --- a/.gitignore +++ b/.gitignore @@ -105,6 +105,8 @@ example/windows/* example/web/* +example/android/app/.cxx + example/\.metadata example/analysis_options.yaml diff --git a/android/build.gradle b/android/build.gradle index 0969e87b..6f89911d 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -16,8 +16,8 @@ rootProject.allprojects { apply plugin: 'com.android.library' android { defaultConfig { - minSdkVersion 16 - compileSdk 31 + minSdkVersion 19 + compileSdk 35 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" multiDexEnabled true @@ -26,6 +26,16 @@ android { disable 'InvalidPackage' } namespace 'com.appsflyer.appsflyersdk' + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = '17' + } + } dependencies { diff --git a/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java b/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java index 26c0eb6e..8b4c05df 100644 --- a/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java +++ b/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java @@ -203,6 +203,7 @@ private void startListening(Object arguments, Result rawResult) { public void onMethodCall(MethodCall call, Result result) { if (activity == null) { Log.d(AF_PLUGIN_TAG, LogMessages.ACTIVITY_NOT_ATTACHED_TO_ENGINE); + result.error("NO_ACTIVITY", "The current activity is null", null); return; } final String method = call.method; diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 4ec36bec..d2667407 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -24,7 +24,7 @@ if (flutterVersionName == null) { android { compileSdkVersion flutter.compileSdkVersion - ndkVersion flutter.ndkVersion + ndkVersion = "27.0.12077973" compileOptions { sourceCompatibility JavaVersion.VERSION_17 diff --git a/lib/src/appsflyer_options.dart b/lib/src/appsflyer_options.dart index 2e6c66b5..62dbb5b3 100644 --- a/lib/src/appsflyer_options.dart +++ b/lib/src/appsflyer_options.dart @@ -15,7 +15,7 @@ class AppsFlyerOptions { /// Requires [afDevKey] and [appId] as mandatory Named parameters. /// All other parameters are optional, it's allows greater flexibility /// when invoking the constructor. - /// When manual start is true the startSDK must be called + /// When [manualStart] is true the startSDK method must be called AppsFlyerOptions({ required this.afDevKey, this.showDebug = false, @@ -27,3 +27,53 @@ class AppsFlyerOptions { this.manualStart = false, }); } + +enum AFMediationNetwork { + ironSource, + applovinMax, + googleAdMob, + fyber, + appodeal, + admost, + topon, + tradplus, + yandex, + chartboost, + unity, + toponPte, + customMediation, + directMonetizationNetwork; + + String get value { + switch (this) { + case AFMediationNetwork.ironSource: + return "ironsource"; + case AFMediationNetwork.applovinMax: + return "applovin_max"; + case AFMediationNetwork.googleAdMob: + return "google_admob"; + case AFMediationNetwork.fyber: + return "fyber"; + case AFMediationNetwork.appodeal: + return "appodeal"; + case AFMediationNetwork.admost: + return "admost"; + case AFMediationNetwork.topon: + return "topon"; + case AFMediationNetwork.tradplus: + return "tradplus"; + case AFMediationNetwork.yandex: + return "yandex"; + case AFMediationNetwork.chartboost: + return "chartboost"; + case AFMediationNetwork.unity: + return "unity"; + case AFMediationNetwork.toponPte: + return "topon_pte"; + case AFMediationNetwork.customMediation: + return "custom_mediation"; + case AFMediationNetwork.directMonetizationNetwork: + return "direct_monetization_network"; + } + } +} From ec30faa4f00d9026c7e68290b22daee879a0c397 Mon Sep 17 00:00:00 2001 From: Dani-Koza-AF Date: Thu, 10 Apr 2025 09:31:29 +0300 Subject: [PATCH 21/42] lint --- android/build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/android/build.gradle b/android/build.gradle index 6f89911d..7819fa93 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -14,6 +14,8 @@ rootProject.allprojects { } } apply plugin: 'com.android.library' +apply plugin: 'org.jetbrains.kotlin.android' + android { defaultConfig { minSdkVersion 19 From ca4743d8a2b2bfdea3b440e50081a0f6253d1783 Mon Sep 17 00:00:00 2001 From: Dani-Koza-AF Date: Thu, 10 Apr 2025 10:57:47 +0300 Subject: [PATCH 22/42] Fixes of tests and typos --- doc/API.md | 8 +- doc/AdvancedAPI.md | 4 +- lib/src/appsflyer_invite_link_params.dart | 2 +- lib/src/appsflyer_sdk.dart | 10 +- test/appsflyer_sdk_test.dart | 220 +++++++++++++--------- 5 files changed, 148 insertions(+), 96 deletions(-) diff --git a/doc/API.md b/doc/API.md index 2041e824..a22d09ac 100644 --- a/doc/API.md +++ b/doc/API.md @@ -175,7 +175,7 @@ await _appsflyerSdk.initSdk( --- ##### **`startSDK()` (Added in 6.13.0)** -In version 6.13.0 of the appslfyer-flutter-plugin SDK we added the option of splitting between the initialization stage and start stage. All you need to do is add the property manualStart: true to the init object, and later call appsFlyer.startSdk() whenever you decide. If this property is set to false or doesn’t exist, the sdk will start after calling appsFlyer.initSdk(...). +In version 6.13.0 of the appslfyer-flutter-plugin SDK we added the option of splitting between the initialization stage and start stage. All you need to do is add the property manualStart: true to the init object, and later call appsFlyer.startSdk() whenever you decide. If this property is set to false or doesn't exist, the sdk will start after calling appsFlyer.initSdk(...). ```dart _appsflyerSdk.startSDK(); ``` @@ -648,7 +648,7 @@ class AppsFlyerInviteLinkParams { final String channel; final String campaign; final String referrerName; - final String referreImageUrl; + final String referrerImageUrl; final String customerID; final String baseDeepLink; final String brandDomain; @@ -673,7 +673,7 @@ AppsFlyerInviteLinkParams inviteLinkParams = new AppsFlyerInviteLinkParams( baseDeepLink: "", brandDomain: "", customerID: "", - referreImageUrl: "", + referrerImageUrl: "", campaign: "", customParams: {"key":"value"} ); @@ -837,7 +837,7 @@ _Example:_ **Android Only!** Enables manual triggering of deep link resolution. This method allows apps that are delaying the call to `appsflyerSdk.startSDK()` to resolve deep links before the SDK starts.
-Note:
This API will trigger the `appsflyerSdk.onDeepLink` callback. In the following example, we check if `res.deepLinkStatus` is equal to “FOUND” inside `appsflyerSdk.onDeepLink` callback to extract the deeplink parameters. +Note:
This API will trigger the `appsflyerSdk.onDeepLink` callback. In the following example, we check if `res.deepLinkStatus` is equal to "FOUND" inside `appsflyerSdk.onDeepLink` callback to extract the deeplink parameters. ```dart void afStart() async { diff --git a/doc/AdvancedAPI.md b/doc/AdvancedAPI.md index 5dd04f3a..07857834 100644 --- a/doc/AdvancedAPI.md +++ b/doc/AdvancedAPI.md @@ -64,7 +64,7 @@ class AppsFlyerInviteLinkParams { final String channel; final String campaign; final String referrerName; - final String referreImageUrl; + final String referrerImageUrl; final String customerID; final String baseDeepLink; final String brandDomain; @@ -89,7 +89,7 @@ AppsFlyerInviteLinkParams inviteLinkParams = new AppsFlyerInviteLinkParams( baseDeepLink: "", brandDomain: "", customerID: "", - referreImageUrl: "", + referrerImageUrl: "", campaign: "", customParams: {"key":"value"} ); diff --git a/lib/src/appsflyer_invite_link_params.dart b/lib/src/appsflyer_invite_link_params.dart index a4494dea..fe169da5 100644 --- a/lib/src/appsflyer_invite_link_params.dart +++ b/lib/src/appsflyer_invite_link_params.dart @@ -5,7 +5,7 @@ class AppsFlyerInviteLinkParams { final String? channel; final String? campaign; final String? referrerName; - final String? referreImageUrl; + final String? referrerImageUrl; final String? customerID; final String? baseDeepLink; final String? brandDomain; diff --git a/lib/src/appsflyer_sdk.dart b/lib/src/appsflyer_sdk.dart index c4cde1bd..d7028795 100644 --- a/lib/src/appsflyer_sdk.dart +++ b/lib/src/appsflyer_sdk.dart @@ -113,8 +113,9 @@ class AppsflyerSdk { } if (options[AppsflyerConstants.AF_MANUAL_START] != null) { - afOptions[AppsflyerConstants.AF_MANUAL_START] = options[AppsflyerConstants.AF_MANUAL_START]; - }else{ + afOptions[AppsflyerConstants.AF_MANUAL_START] = + options[AppsflyerConstants.AF_MANUAL_START]; + } else { afOptions[AppsflyerConstants.AF_MANUAL_START] = false; } @@ -237,7 +238,6 @@ class AppsflyerSdk { "logEvent", {'eventName': eventName, 'eventValues': eventValues}); } - /// Log ad revenue API. void logAdRevenue(AdRevenueData adRevenueData) { _methodChannel.invokeMethod("logAdRevenue", adRevenueData.toMap()); @@ -325,7 +325,7 @@ class AppsflyerSdk { _methodChannel.invokeMethod("performOnDeepLinking"); } - /// Setting your own customer ID enables you to cross-reference your own unique ID with AppsFlyer’s unique ID and the other devices’ IDs. + /// Setting your own customer ID enables you to cross-reference your own unique ID with AppsFlyer's unique ID and the other devices' IDs. /// This ID is available in AppsFlyer CSV reports along with Postback APIs for cross-referencing with your internal IDs. void setCustomerUserId(String id) { _methodChannel.invokeMethod("setCustomerUserId", {'id': id}); @@ -452,7 +452,7 @@ class AppsflyerSdk { AppsFlyerInviteLinkParams params) { Map inviteLinkParamsMap = {}; inviteLinkParamsMap['customParams'] = params.customParams; - inviteLinkParamsMap['referrerImageUrl'] = params.referreImageUrl; + inviteLinkParamsMap['referrerImageUrl'] = params.referrerImageUrl; inviteLinkParamsMap['customerID'] = params.customerID; inviteLinkParamsMap['brandDomain'] = params.brandDomain; inviteLinkParamsMap['baseDeeplink'] = params.baseDeepLink; diff --git a/test/appsflyer_sdk_test.dart b/test/appsflyer_sdk_test.dart index ebf06778..3b25f472 100644 --- a/test/appsflyer_sdk_test.dart +++ b/test/appsflyer_sdk_test.dart @@ -17,87 +17,84 @@ void main() { instance = AppsflyerSdk.private(methodChannel, eventChannel, mapOptions: {'afDevKey': 'sdfhj2342cx'}); - methodChannel.setMockMethodCallHandler((methodCall) async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(methodChannel, (methodCall) async { String method = methodCall.method; - if (method == 'initSdk') { - selectedMethod = method; + switch (method) { + case 'initSdk': + case 'setOneLinkCustomDomain': + case 'logCrossPromotionAndOpenStore': + case 'logCrossPromotionImpression': + case 'setAppInviteOneLinkID': + case 'generateInviteLink': + case 'setSharingFilterForAllPartners': + case 'setSharingFilter': + case 'getSDKVersion': + case 'getAppsFlyerUID': + case 'validateAndLogInAppAndroidPurchase': + case 'setMinTimeBetweenSessions': + case 'getHostPrefix': + case 'getHostName': + case 'setCollectIMEI': + case 'setCollectAndroidId': + case 'setUserEmails': + case 'setAdditionalData': + case 'waitForCustomerUserId': + case 'setCustomerUserId': + case 'setAndroidIdData': + case 'setImeiData': + case 'updateServerUninstallToken': + case 'stop': + case 'setIsUpdate': + case 'setCurrencyCode': + case 'setHost': + case 'logEvent': + case 'setOutOfStore': + case 'getOutOfStore': + case 'logAdRevenue': + case 'setConsentData': + case 'enableTCFDataCollection': + case 'setDisableNetworkData': + case 'setPartnerData': + case 'setResolveDeepLinkURLs': + case 'setPushNotification': + case 'sendPushNotificationData': + case 'enableFacebookDeferredApplinks': + case 'disableSKAdNetwork': + case 'setDisableAdvertisingIdentifiers': + selectedMethod = method; + break; } + return null; }); - eventMethodChannel.setMockMethodCallHandler((methodCall) async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(eventMethodChannel, (methodCall) async { String method = methodCall.method; if (method == 'listen') { selectedMethod = method; } + return null; }); }); test('check initSdk call', () async { await instance.initSdk( - registerConversionDataCallback: true, - registerOnAppOpenAttributionCallback: true, - registerOnDeepLinkingCallback: false - ); + registerConversionDataCallback: true, + registerOnAppOpenAttributionCallback: true, + registerOnDeepLinkingCallback: false); expect('initSdk', selectedMethod); }); group('AppsFlyerSdk', () { setUp(() { - //test map options way - instance = AppsflyerSdk.private(methodChannel, eventChannel, - mapOptions: {'afDevKey': 'sdfhj2342cx'}); - - callbacksChannel.setMockMethodCallHandler((call) async { - String method = call.method; - if (method == 'startListening') { - selectedMethod = method; - } - }); - - methodChannel.setMockMethodCallHandler((methodCall) async { - String method = methodCall.method; - switch (method) { - case 'setOneLinkCustomDomain': - case 'logCrossPromotionAndOpenStore': - case 'logCrossPromotionImpression': - case 'setAppInviteOneLinkID': - case 'generateInviteLink': - case 'setSharingFilterForAllPartners': - case 'setSharingFilter': - case 'getSDKVersion': - case 'getAppsFlyerUID': - case 'validateAndLogInAppAndroidPurchase': - case 'setMinTimeBetweenSessions': - case 'getHostPrefix': - case 'getHostName': - case 'setCollectIMEI': - case 'setCollectAndroidId': - case 'setUserEmailsWithCryptType': - case 'setUserEmails': - case 'setAdditionalData': - case 'waitForCustomerUserId': - case 'setCustomerUserId': - case 'enableLocationCollection': - case 'setAndroidIdData': - case 'setImeiData': - case 'updateServerUninstallToken': - case 'stop': - case 'setIsUpdate': - case 'setCurrencyCode': - case 'setHost': - case 'logEvent': - case 'initSdk': - case 'setOutOfStore': - case 'getOutOfStore': - selectedMethod = methodCall.method; - break; - } - }); + selectedMethod = ""; }); tearDown(() { - methodChannel.setMockMethodCallHandler(null); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(methodChannel, null); }); test('check logEvent call', () async { @@ -166,18 +163,6 @@ void main() { expect(selectedMethod, 'generateInviteLink'); }); - test('check setSharingFilterForAllPartners call', () async { - instance.setSharingFilterForAllPartners(); - - expect(selectedMethod, 'setSharingFilterForAllPartners'); - }); - - test('check setSharingFilter call', () async { - instance.setSharingFilter(["filters"]); - - expect(selectedMethod, 'setSharingFilter'); - }); - test('check getSDKVersion call', () async { instance.getSDKVersion(); @@ -227,14 +212,8 @@ void main() { expect(selectedMethod, 'setCollectAndroidId'); }); - test('check setUserEmailsWithCryptType call', () async { - instance.setUserEmails(["emails"], EmailCryptType.EmailCryptTypeNone); - - expect(selectedMethod, 'setUserEmailsWithCryptType'); - }); - test('check setUserEmails call', () async { - instance.setUserEmails(["emails"]); + instance.setUserEmails(["emails"], EmailCryptType.EmailCryptTypeNone); expect(selectedMethod, 'setUserEmails'); }); @@ -257,12 +236,6 @@ void main() { expect(selectedMethod, 'setCustomerUserId'); }); - test('check enableLocationCollection call', () async { - //instance.enableLocationCollection(false); - - expect(selectedMethod, 'enableLocationCollection'); - }); - test('check setImeiData call', () async { instance.setImeiData("imei"); @@ -286,5 +259,84 @@ void main() { expect(selectedMethod, 'setOutOfStore'); }); + + test('check logAdRevenue call', () async { + final adRevenueData = AdRevenueData( + monetizationNetwork: 'GoogleAdMob', + mediationNetwork: AFMediationNetwork.googleAdMob.value, + currencyIso4217Code: 'USD', + revenue: 1.23, + additionalParameters: { + 'adUnitId': 'ca-app-pub-XXXX/YYYY', + 'ad_network_click_id': '12345' + }); + instance.logAdRevenue(adRevenueData); + + expect(selectedMethod, 'logAdRevenue'); + }); + + test('check setConsentData call', () async { + final consentData = AppsFlyerConsent.forGDPRUser( + hasConsentForDataUsage: true, + hasConsentForAdsPersonalization: true, + ); + instance.setConsentData(consentData); + + expect(selectedMethod, 'setConsentData'); + }); + + test('check enableTCFDataCollection call', () async { + instance.enableTCFDataCollection(true); + + expect(selectedMethod, 'enableTCFDataCollection'); + }); + + test('check setDisableNetworkData call', () async { + instance.setDisableNetworkData(true); + + expect(selectedMethod, 'setDisableNetworkData'); + }); + + test('check setPartnerData call', () async { + instance.setPartnerData('partnerId', {'key': 'value'}); + + expect(selectedMethod, 'setPartnerData'); + }); + + test('check setResolveDeepLinkURLs call', () async { + instance.setResolveDeepLinkURLs(['https://example.com']); + + expect(selectedMethod, 'setResolveDeepLinkURLs'); + }); + + test('check setPushNotification call', () async { + instance.setPushNotification(true); + + expect(selectedMethod, 'setPushNotification'); + }); + + test('check sendPushNotificationData call', () async { + instance.sendPushNotificationData({'key': 'value'}); + + expect(selectedMethod, 'sendPushNotificationData'); + }); + + test('check enableFacebookDeferredApplinks call', () async { + instance.enableFacebookDeferredApplinks(true); + + expect(selectedMethod, 'enableFacebookDeferredApplinks'); + }); + + test('check disableSKAdNetwork call', () async { + instance.disableSKAdNetwork(true); + + expect(selectedMethod, 'disableSKAdNetwork'); + }); + + test('check setDisableAdvertisingIdentifiers call', () async { + instance.setDisableAdvertisingIdentifiers(true); + + expect(selectedMethod, 'setDisableAdvertisingIdentifiers'); + }); }); } From e086a26be095d0f2653b90a7a4ca701797d9cf77 Mon Sep 17 00:00:00 2001 From: Dani Koza <103039399+Dani-Koza-AF@users.noreply.github.com> Date: Mon, 28 Apr 2025 10:42:59 +0300 Subject: [PATCH 23/42] Dev/update manual consent api (#383) New consent api and more - setConsentData is now deprecated. - setConsentDataV2 is the new and recommended way to set manual user consent. - Add getVersionNumber api, returns the plugin version. - version bumps. - typos fix. - doc updates. - bug fix. --- .gitignore | 1 - android/build.gradle | 2 +- .../appsflyersdk/AppsflyerSdkPlugin.java | 39 ++++ .../appsflyer/appsflyersdk/LogMessages.java | 1 + doc/API.md | 34 +++- doc/DMA.md | 190 ++++++++++++++++++ .../xcshareddata/xcschemes/Runner.xcscheme | 3 +- example/ios/Runner/AppDelegate.swift | 2 +- ios/Classes/AppsflyerSdkPlugin.h | 2 +- ios/Classes/AppsflyerSdkPlugin.m | 45 ++++- ios/appsflyer_sdk.podspec | 4 +- lib/appsflyer_sdk.dart | 7 +- lib/src/appsflyer_constants.dart | 51 +++++ lib/src/appsflyer_invite_link_params.dart | 2 +- lib/src/appsflyer_options.dart | 50 ----- lib/src/appsflyer_sdk.dart | 25 +++ pubspec.yaml | 2 +- 17 files changed, 394 insertions(+), 66 deletions(-) create mode 100644 doc/DMA.md diff --git a/.gitignore b/.gitignore index 2f5397af..591007ae 100644 --- a/.gitignore +++ b/.gitignore @@ -116,4 +116,3 @@ node_modules/ covBadgeGen.js coverage/ .env - diff --git a/android/build.gradle b/android/build.gradle index 7819fa93..fc69f9fc 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -43,6 +43,6 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.appcompat:appcompat:1.0.0' - implementation 'com.appsflyer:af-android-sdk:6.15.2' + implementation 'com.appsflyer:af-android-sdk:6.16.2' implementation 'com.android.installreferrer:installreferrer:2.1' } \ No newline at end of file diff --git a/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java b/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java index 8b4c05df..5c053485 100644 --- a/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java +++ b/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java @@ -52,6 +52,8 @@ import static com.appsflyer.appsflyersdk.AppsFlyerConstants.AF_PLUGIN_TAG; import static com.appsflyer.appsflyersdk.AppsFlyerConstants.AF_SUCCESS; +import androidx.annotation.NonNull; + /** * AppsflyerSdkPlugin */ @@ -232,6 +234,9 @@ public void onMethodCall(MethodCall call, Result result) { case "setConsentData": setConsentData(call, result); break; + case "setConsentDataV2": + setConsentDataV2(call, result); + break; case "setIsUpdate": setIsUpdate(call, result); break; @@ -423,6 +428,11 @@ private void startSDK(MethodCall call, final Result result) { result.success(null); } + /** + * Sets the user consent data for tracking. + * @deprecated Use {@link #setConsentDataV2(MethodCall, Result)} instead. + */ + @Deprecated public void setConsentData(MethodCall call, Result result) { Map arguments = (Map) call.arguments; Map consentDict = (Map) arguments.get("consentData"); @@ -444,6 +454,35 @@ public void setConsentData(MethodCall call, Result result) { result.success(null); } + /** + * Sets the user consent data for tracking with flexible parameters. + */ + public void setConsentDataV2(MethodCall call, Result result) { + try { + AppsFlyerConsent consent = getAppsFlyerConsentFromCall(call); + AppsFlyerLib.getInstance().setConsentData(consent); + result.success(null); + } catch (Exception e) { + Log.e(AF_PLUGIN_TAG, LogMessages.ERROR_WHILE_SETTING_CONSENT + e.getMessage(), e); + result.error("CONSENT_ERROR", LogMessages.ERROR_WHILE_SETTING_CONSENT + e.getMessage(), null); + } + } + + @NonNull + @SuppressWarnings("unchecked") + private AppsFlyerConsent getAppsFlyerConsentFromCall(MethodCall call) { + Map args = (Map) call.arguments; + + // Extract nullable Boolean arguments + Boolean isUserSubjectToGDPR = (Boolean) args.get("isUserSubjectToGDPR"); + Boolean consentForDataUsage = (Boolean) args.get("consentForDataUsage"); + Boolean consentForAdsPersonalization = (Boolean) args.get("consentForAdsPersonalization"); + Boolean hasConsentForAdStorage = (Boolean) args.get("hasConsentForAdStorage"); + + // Create and return AppsFlyerConsent object with the given parameters + return new AppsFlyerConsent(isUserSubjectToGDPR, consentForDataUsage, consentForAdsPersonalization, hasConsentForAdStorage); + } + private void enableTCFDataCollection(MethodCall call, Result result) { boolean shouldCollect = (boolean) call.argument("shouldCollect"); AppsFlyerLib.getInstance().enableTCFDataCollection(shouldCollect); diff --git a/android/src/main/java/com/appsflyer/appsflyersdk/LogMessages.java b/android/src/main/java/com/appsflyer/appsflyersdk/LogMessages.java index cc363ef3..8adf9747 100644 --- a/android/src/main/java/com/appsflyer/appsflyersdk/LogMessages.java +++ b/android/src/main/java/com/appsflyer/appsflyersdk/LogMessages.java @@ -9,4 +9,5 @@ private LogMessages() { public static final String METHOD_CHANNEL_IS_NULL = "mMethodChannel is null, cannot invoke the callback"; public static final String ACTIVITY_NOT_ATTACHED_TO_ENGINE = "Activity isn't attached to the flutter engine"; + public static final String ERROR_WHILE_SETTING_CONSENT = "Error while setting consent data: "; } diff --git a/doc/API.md b/doc/API.md index a22d09ac..1b6327d1 100644 --- a/doc/API.md +++ b/doc/API.md @@ -40,7 +40,8 @@ - [User Invite](#userInvite) - [enableFacebookDeferredApplinks](#enableFacebookDeferredApplinks) - [enableTCFDataCollection](#enableTCFDataCollection) -- [setConsentData](#setConsentData) +- [setConsentData](#setConsentData) - [DEPRECATED] +- [setConsentDataV2](#setConsentDataV2) - [disableSKAdNetwork](#disableSKAdNetwork) - [getAppsFlyerUID](#getAppsFlyerUID) - [setCurrentDeviceLanguage](#setCurrentDeviceLanguage) @@ -331,7 +332,7 @@ _Example:_ appsFlyerSdk.enableTCFDataCollection(true); ``` --- -**
`setConsentData(Map consentData)`** +** `setConsentData(Map consentData)`** *Deprecated* The `AppsflyerConsent` object helps manage user consent settings. By using the setConsentData we able to manually collect the TCF data. You can create an instance for users subject to GDPR or otherwise: @@ -393,6 +394,35 @@ _appsflyerSdk.startSDK(); Following this sequence ensures that the consent configurations take effect before the AppsFlyer SDK starts, providing accurate consent data in the first launch payload. Note: You need to use either `enableTCFDataCollection` or `setConsentData` if you use both of them our backend will prioritize the provided consent data from `setConsentData`. +--- +** `setConsentDataV2({bool? isUserSubjectToGDPR, bool? consentForDataUsage, bool? consentForAdsPersonalization, bool? hasConsentForAdStorage})`** + +### Sets user consent preferences for GDPR and ad personalization + +> âš ī¸ This method replaces the deprecated `setConsentData` - for a complete guide, see our [DMA compliance documentation](DMA.md). + +Use this method to provide the user's consent settings to the AppsFlyer SDK. All parameters are optional - you only need to include the ones relevant to your use case. + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `isUserSubjectToGDPR` | `bool?` | Whether the user is subject to GDPR regulations | +| `consentForDataUsage` | `bool?` | Whether the user consents to data usage by AppsFlyer | +| `consentForAdsPersonalization` | `bool?` | Whether the user consents to personalized advertising | +| `hasConsentForAdStorage` | `bool?` | Whether the user consents to ad storage | + +> 📝 **Note:** Setting a parameter to `null` indicates the user hasn't explicitly provided consent for that option. + +_Example:_ +```dart +appsflyerSdk.setConsentDataV2( + isUserSubjectToGDPR: true, + consentForDataUsage: true, + consentForAdsPersonalization: false, + hasConsentForAdStorage: true, +); +``` --- ** `void setCustomerUserId(String userId)`** diff --git a/doc/DMA.md b/doc/DMA.md new file mode 100644 index 00000000..92d44c32 --- /dev/null +++ b/doc/DMA.md @@ -0,0 +1,190 @@ +# Set Consent For DMA Compliance + +Following the DMA regulations that were set by the European Commission, Google (and potentially other SRNs in the future) require to send them the user's consent data in order to interact with them during the attribution process. In our latest plugin update (6.16.2), we've introduced two new public APIs, enhancing our support for user consent and data collection preferences in line with evolving digital market regulations. +There are two alternative ways for gathering consent data: + +- Through a Consent Management Platform (CMP): If the app uses a CMP that complies with the Transparency and Consent Framework (TCF) v2.2 protocol, the SDK can automatically retrieve the consent details. +### OR +- Through a dedicated SDK API: Developers can pass Google's required consent data directly to the SDK using a specific API designed for this purpose. + +## Use CMP to collect consent data +A CMP compatible with TCF v2.2 collects DMA consent data and stores it in NSUserDefaults (iOS) and SharedPreferences (Android). To enable the SDK to access this data and include it with every event, follow these steps: +1. Call `appsflyerSdk.enableTCFDataCollection(true)` +2. Initialize the SDK in manual start mode by setting `manualStart: true` in the `AppsFlyerOptions` when creating the AppsflyerSdk instance. +3. Use the CMP to decide if you need the consent dialog in the current session to acquire the consent data. If you need the consent dialog move to step 4, otherwise move to step 5. +4. Get confirmation from the CMP that the user has made their consent decision and the data is available in NSUserDefaults/SharedPreferences. +5. Call `appsflyerSdk.startSDK()` + +```dart +// Initialize AppsFlyerOptions with manualStart: true +final AppsFlyerOptions options = AppsFlyerOptions( + afDevKey: 'your_dev_key', + appId: '1234567890', // Required for iOS only + showDebug: true, + manualStart: true // <--- Manual Start +); + +// Create the AppsflyerSdk instance +AppsflyerSdk appsflyerSdk = AppsflyerSdk(options); + +// Initialize the SDK +appsflyerSdk.initSdk( + registerConversionDataCallback: true, + registerOnAppOpenAttributionCallback: true, + registerOnDeepLinkingCallback: true +); + +// CMP pseudocode procedure +if (cmpManager.hasConsent()) { + appsflyerSdk.startSDK(); +} else { + cmpManager.presentConsentDialogToUser() + .then((_) => appsflyerSdk.startSDK()); +} +``` + +## Manually collect consent data +### setConsentData is now **deprecated**. use setConsentDataV2 +If your app does not use a CMP compatible with TCF v2.2, use the SDK API detailed below to provide the consent data directly to the SDK, distinguishing between cases when GDPR applies or not. + +### When GDPR applies to the user +If GDPR applies to the user, perform the following: + +1. Given that GDPR is applicable to the user, determine whether the consent data is already stored for this session. + 1. If there is no consent data stored, show the consent dialog to capture the user consent decision. + 2. If there is consent data stored continue to the next step. +2. To transfer the consent data to the SDK create an AppsFlyerConsent object using `forGDPRUser` method that accepts the following parameters:
+ `hasConsentForDataUsage: boolean` - Indicates whether the user has consented to use their data for advertising purposes.
+ `hasConsentForAdsPersonalization: boolean` - Indicates whether the user has consented to use their data for personalized advertising. +3. Call `appsflyerSdk.setConsentData(consentData)` with the AppsFlyerConsent object. +4. Initialize the SDK using `appsflyerSdk.initSdk()`. + +```dart +// If the user is subject to GDPR - collect the consent data +// or retrieve it from the storage +// ... + +// Set the consent data to the SDK: +var gdprConsent = AppsFlyerConsent.forGDPRUser( + hasConsentForDataUsage: true, + hasConsentForAdsPersonalization: false +); + +appsflyerSdk.setConsentData(gdprConsent); + +// Initialize AppsFlyerOptions +final AppsFlyerOptions options = AppsFlyerOptions( + afDevKey: 'your_dev_key', + appId: '1234567890', // Required for iOS only + showDebug: true +); + +// Create the AppsflyerSdk instance +AppsflyerSdk appsflyerSdk = AppsflyerSdk(options); + +// Initialize the SDK +appsflyerSdk.initSdk( + registerConversionDataCallback: true, + registerOnAppOpenAttributionCallback: true, + registerOnDeepLinkingCallback: true +); +``` + +### When GDPR does not apply to the user + +If GDPR doesn't apply to the user perform the following: +1. Create an AppsFlyerConsent object using `nonGDPRUser` method that doesn't accept any parameters. +2. Call `appsflyerSdk.setConsentData(consentData)` with the AppsFlyerConsent object. +3. Initialize the SDK using `appsflyerSdk.initSdk()`. + +```dart +// If the user is not subject to GDPR: +var nonGdprUserConsentData = AppsFlyerConsent.nonGDPRUser(); + +appsflyerSdk.setConsentData(nonGdprUserConsentData); + +// Initialize AppsFlyerOptions +final AppsFlyerOptions options = AppsFlyerOptions( + afDevKey: 'your_dev_key', + appId: '1234567890', // Required for iOS only + showDebug: true +); + +// Create the AppsflyerSdk instance +AppsflyerSdk appsflyerSdk = AppsflyerSdk(options); + +// Initialize the SDK +appsflyerSdk.initSdk( + registerConversionDataCallback: true, + registerOnAppOpenAttributionCallback: true, + registerOnDeepLinkingCallback: true +); +``` + +## setConsentDataV2 (Recommended API for Manual Consent Collection) - since 6.16.2 +🚀 **Why Use setConsentDataV2?**
+The setConsentDataV2 API is the new and improved way to manually provide user consent data to the AppsFlyer SDK. + +It replaces the now deprecated setConsentData method, offering several improvements:
+✅ **Simpler and More Intuitive:** Accepts named parameters, making it easier to manage.
+✅ **Includes an Additional Consent Parameter:** Now supports hasConsentForAdStorage to give users more granular control over their data.
+✅ **Enhanced Clarity**: Allows nullable boolean values, indicating when users have not provided consent instead of forcing defaults.
+✅ **Future-Proof:** Designed to be aligned with evolving privacy regulations and best practices.
+ +If your app previously used setConsentData, it is highly recommended to migrate to setConsentDataV2 for a more flexible and robust solution. + +📌 **API Reference** +```dart +void setConsentDataV2({ + bool? isUserSubjectToGDPR, + bool? consentForDataUsage, + bool? consentForAdsPersonalization, + bool? hasConsentForAdStorage +}) +``` + +**Parameters** +| Parameter | Type | Description | +| -------- | -------- | -------- | +| isUserSubjectToGDPR | bool? | Indicates if the user is subject to GDPR regulations. | +| consentForDataUsage | bool? | Determines if the user consents to data usage. | +| consentForAdsPersonalization | bool? | Determines if the user consents to personalized ads. | +| hasConsentForAdStorage | bool? | **(New!)** Determines if the user consents to storing ad-related data.| + +- If a parameter is `null`, it means the user has **not explicitly provided consent** for that option. +- These values should be collected from the user via an appropriate **UI or consent prompt** before calling this method. + +📌 **Example Usage** +```dart +// Initialize AppsFlyerOptions with manualStart: true +final AppsFlyerOptions options = AppsFlyerOptions( + afDevKey: 'your_dev_key', + appId: '1234567890', // Required for iOS only + showDebug: true, + manualStart: true +); + +// Create the AppsflyerSdk instance +AppsflyerSdk appsflyerSdk = AppsflyerSdk(options); + +// Set consent data BEFORE initializing the SDK +appsflyerSdk.setConsentDataV2( + isUserSubjectToGDPR: true, + consentForDataUsage: true, + consentForAdsPersonalization: false, + hasConsentForAdStorage: null // User has not explicitly provided consent +); + +// Initialize the SDK +appsflyerSdk.initSdk( + registerConversionDataCallback: true, + registerOnAppOpenAttributionCallback: true, + registerOnDeepLinkingCallback: true +); + +// Start the SDK +appsflyerSdk.startSDK(); +``` +📌 **Notes**
+â€ĸ You should call this method **before initializing the AppsFlyer SDK** if possible, or at least before `startSDK()` when using manual initialization.
+â€ĸ Ensure you collect consent **legally and transparently** from the user before passing these values. diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 87131a09..15cada48 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift index 70693e4a..b6363034 100644 --- a/example/ios/Runner/AppDelegate.swift +++ b/example/ios/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import UIKit import Flutter -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/ios/Classes/AppsflyerSdkPlugin.h b/ios/Classes/AppsflyerSdkPlugin.h index 0ae703a8..3bc77470 100644 --- a/ios/Classes/AppsflyerSdkPlugin.h +++ b/ios/Classes/AppsflyerSdkPlugin.h @@ -18,7 +18,7 @@ @end // Appsflyer JS objects -#define kAppsFlyerPluginVersion @"6.15.1" +#define kAppsFlyerPluginVersion @"6.16.2" #define afDevKey @"afDevKey" #define afAppId @"afAppId" #define afIsDebug @"isDebug" diff --git a/ios/Classes/AppsflyerSdkPlugin.m b/ios/Classes/AppsflyerSdkPlugin.m index f497c68a..8f4a0b11 100644 --- a/ios/Classes/AppsflyerSdkPlugin.m +++ b/ios/Classes/AppsflyerSdkPlugin.m @@ -157,6 +157,8 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { [self enableTCFDataCollection:call result:result]; }else if([@"setConsentData" isEqualToString:call.method]){ [self setConsentData:call result:result]; + }else if([@"setConsentDataV2" isEqualToString:call.method]){ + [self setConsentDataV2:call result:result]; }else if([@"logAdRevenue" isEqualToString:call.method]){ [self logAdRevenue:call result:result]; } @@ -201,13 +203,54 @@ - (void)setConsentData:(FlutterMethodCall*)call result:(FlutterResult)result { consentData = [[AppsFlyerConsent alloc] initForGDPRUserWithHasConsentForDataUsage:hasConsentForDataUsage hasConsentForAdsPersonalization:hasConsentForAdsPersonalization]; }else{ - consentData = [[AppsFlyerConsent alloc] initNonGDPRUser]; + consentData = [[AppsFlyerConsent alloc] initWithNonGDPRUser]; } [[AppsFlyerLib shared] setConsentData:consentData]; result(nil); } +- (void)setConsentDataV2:(FlutterMethodCall*)call result:(FlutterResult)result { + @try { + // Extract the parameters directly from the arguments + NSNumber *isUserSubjectToGDPR = call.arguments[@"isUserSubjectToGDPR"]; + if ([isUserSubjectToGDPR isKindOfClass:[NSNull class]]) { + isUserSubjectToGDPR = nil; + } + + NSNumber *consentForDataUsage = call.arguments[@"consentForDataUsage"]; + if ([consentForDataUsage isKindOfClass:[NSNull class]]) { + consentForDataUsage = nil; + } + + NSNumber *consentForAdsPersonalization = call.arguments[@"consentForAdsPersonalization"]; + if ([consentForAdsPersonalization isKindOfClass:[NSNull class]]) { + consentForAdsPersonalization = nil; + } + + NSNumber *hasConsentForAdStorage = call.arguments[@"hasConsentForAdStorage"]; + if ([hasConsentForAdStorage isKindOfClass:[NSNull class]]) { + hasConsentForAdStorage = nil; + } + + // Create the consent object + AppsFlyerConsent *consentData = [[AppsFlyerConsent alloc] initWithIsUserSubjectToGDPR:isUserSubjectToGDPR + hasConsentForDataUsage:consentForDataUsage + hasConsentForAdsPersonalization:consentForAdsPersonalization + hasConsentForAdStorage:hasConsentForAdStorage]; + + // Set the consent data using AppsFlyer SDK + [[AppsFlyerLib shared] setConsentData:consentData]; + result(nil); + } + @catch (NSException *exception) { + NSLog(@"AppsFlyer: Error setting consent data v2: %@", exception.reason); + result([FlutterError errorWithCode:@"CONSENT_ERROR" + message:[NSString stringWithFormat:@"Failed to set consent data v2: %@", exception.reason] + details:nil]); + } +} + - (void)logAdRevenue:(FlutterMethodCall*)call result:(FlutterResult)result { @try { NSString *monetizationNetwork = [self requireNonNullArgumentWithCall:call result:result argumentName:@"monetizationNetwork" errorCode:@"NULL_MONETIZATION_NETWORK"]; diff --git a/ios/appsflyer_sdk.podspec b/ios/appsflyer_sdk.podspec index c0305457..6a2631e5 100644 --- a/ios/appsflyer_sdk.podspec +++ b/ios/appsflyer_sdk.podspec @@ -3,7 +3,7 @@ # Pod::Spec.new do |s| s.name = 'appsflyer_sdk' - s.version = '6.15.3' + s.version = '6.16.2' s.summary = 'AppsFlyer Integration for Flutter' s.description = <<-DESC AppsFlyer is the market leader in mobile advertising attribution & analytics, helping marketers to pinpoint their targeting, optimize their ad spend and boost their ROI. @@ -21,5 +21,5 @@ AppsFlyer is the market leader in mobile advertising attribution & analytics, he s.source_files = 'Classes/**/*' s.public_header_files = 'Classes/**/*.h' s.dependency 'Flutter' - s.ios.dependency 'AppsFlyerFramework','6.15.3' + s.ios.dependency 'AppsFlyerFramework','6.16.2' end diff --git a/lib/appsflyer_sdk.dart b/lib/appsflyer_sdk.dart index 1d8ab749..cee5ef4e 100644 --- a/lib/appsflyer_sdk.dart +++ b/lib/appsflyer_sdk.dart @@ -7,15 +7,14 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; - import 'src/callbacks.dart'; +part 'src/appsflyer_ad_revenue_data.dart'; part 'src/appsflyer_constants.dart'; +part 'src/appsflyer_consent.dart'; part 'src/appsflyer_invite_link_params.dart'; part 'src/appsflyer_options.dart'; +part 'src/appsflyer_request_listener.dart'; part 'src/appsflyer_sdk.dart'; part 'src/udl/deep_link_result.dart'; part 'src/udl/deeplink.dart'; -part 'src/appsflyer_consent.dart'; -part 'src/appsflyer_request_listener.dart'; -part 'src/appsflyer_ad_revenue_data.dart'; diff --git a/lib/src/appsflyer_constants.dart b/lib/src/appsflyer_constants.dart index 9eefe3ce..cd1c447e 100644 --- a/lib/src/appsflyer_constants.dart +++ b/lib/src/appsflyer_constants.dart @@ -3,6 +3,7 @@ part of appsflyer_sdk; enum EmailCryptType { EmailCryptTypeNone, EmailCryptTypeSHA256 } class AppsflyerConstants { + static const String PLUGIN_VERSION = "6.16.2"; static const String AF_DEV_KEY = "afDevKey"; static const String AF_APP_Id = "afAppId"; static const String AF_IS_DEBUG = "isDebug"; @@ -27,3 +28,53 @@ class AppsflyerConstants { static const String DISABLE_ADVERTISING_IDENTIFIER = "disableAdvertisingIdentifier"; } + +enum AFMediationNetwork { + ironSource, + applovinMax, + googleAdMob, + fyber, + appodeal, + admost, + topon, + tradplus, + yandex, + chartboost, + unity, + toponPte, + customMediation, + directMonetizationNetwork; + + String get value { + switch (this) { + case AFMediationNetwork.ironSource: + return "ironsource"; + case AFMediationNetwork.applovinMax: + return "applovin_max"; + case AFMediationNetwork.googleAdMob: + return "google_admob"; + case AFMediationNetwork.fyber: + return "fyber"; + case AFMediationNetwork.appodeal: + return "appodeal"; + case AFMediationNetwork.admost: + return "admost"; + case AFMediationNetwork.topon: + return "topon"; + case AFMediationNetwork.tradplus: + return "tradplus"; + case AFMediationNetwork.yandex: + return "yandex"; + case AFMediationNetwork.chartboost: + return "chartboost"; + case AFMediationNetwork.unity: + return "unity"; + case AFMediationNetwork.toponPte: + return "topon_pte"; + case AFMediationNetwork.customMediation: + return "custom_mediation"; + case AFMediationNetwork.directMonetizationNetwork: + return "direct_monetization_network"; + } + } +} diff --git a/lib/src/appsflyer_invite_link_params.dart b/lib/src/appsflyer_invite_link_params.dart index fe169da5..772d23c1 100644 --- a/lib/src/appsflyer_invite_link_params.dart +++ b/lib/src/appsflyer_invite_link_params.dart @@ -21,7 +21,7 @@ class AppsFlyerInviteLinkParams { this.baseDeepLink, this.brandDomain, this.customerID, - this.referreImageUrl, + this.referrerImageUrl, this.customParams }); } diff --git a/lib/src/appsflyer_options.dart b/lib/src/appsflyer_options.dart index 62dbb5b3..240f5556 100644 --- a/lib/src/appsflyer_options.dart +++ b/lib/src/appsflyer_options.dart @@ -27,53 +27,3 @@ class AppsFlyerOptions { this.manualStart = false, }); } - -enum AFMediationNetwork { - ironSource, - applovinMax, - googleAdMob, - fyber, - appodeal, - admost, - topon, - tradplus, - yandex, - chartboost, - unity, - toponPte, - customMediation, - directMonetizationNetwork; - - String get value { - switch (this) { - case AFMediationNetwork.ironSource: - return "ironsource"; - case AFMediationNetwork.applovinMax: - return "applovin_max"; - case AFMediationNetwork.googleAdMob: - return "google_admob"; - case AFMediationNetwork.fyber: - return "fyber"; - case AFMediationNetwork.appodeal: - return "appodeal"; - case AFMediationNetwork.admost: - return "admost"; - case AFMediationNetwork.topon: - return "topon"; - case AFMediationNetwork.tradplus: - return "tradplus"; - case AFMediationNetwork.yandex: - return "yandex"; - case AFMediationNetwork.chartboost: - return "chartboost"; - case AFMediationNetwork.unity: - return "unity"; - case AFMediationNetwork.toponPte: - return "topon_pte"; - case AFMediationNetwork.customMediation: - return "custom_mediation"; - case AFMediationNetwork.directMonetizationNetwork: - return "direct_monetization_network"; - } - } -} diff --git a/lib/src/appsflyer_sdk.dart b/lib/src/appsflyer_sdk.dart index d7028795..45cf4bae 100644 --- a/lib/src/appsflyer_sdk.dart +++ b/lib/src/appsflyer_sdk.dart @@ -309,11 +309,31 @@ class AppsflyerSdk { "enableTCFDataCollection", {'shouldCollect': shouldCollect}); } + @Deprecated('Use setConsentDataV2 instead') void setConsentData(AppsFlyerConsent consentData) { _methodChannel.invokeMethod('setConsentData', {'consentData': consentData.toMap()}); } + /// Sets the user consent data. + /// + /// [isUserSubjectToGDPR] - Indicates whether the user is subject to GDPR regulations. + /// [consentForDataUsage] - Indicates whether the user consents to data usage by AppsFlyer. + /// [consentForAdsPersonalization] - Indicates whether the user consents to ad personalization. + /// [hasConsentForAdStorage] - Indicates whether the user consents to ad storage. + void setConsentDataV2( + {bool? isUserSubjectToGDPR, + bool? consentForDataUsage, + bool? consentForAdsPersonalization, + bool? hasConsentForAdStorage}) { + _methodChannel.invokeMethod('setConsentDataV2', { + 'isUserSubjectToGDPR': isUserSubjectToGDPR, + 'consentForDataUsage': consentForDataUsage, + 'consentForAdsPersonalization': consentForAdsPersonalization, + 'hasConsentForAdStorage': hasConsentForAdStorage, + }); + } + /// Opt-out logging for specific user void anonymizeUser(bool shouldAnonymize) { _methodChannel @@ -592,4 +612,9 @@ class AppsflyerSdk { void setDisableNetworkData(bool disable) { _methodChannel.invokeMethod("setDisableNetworkData", disable); } + + /// Retrieves the current plugin version. + String getVersionNumber() { + return AppsflyerConstants.PLUGIN_VERSION; + } } diff --git a/pubspec.yaml b/pubspec.yaml index cb6c3695..9645e263 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: appsflyer_sdk description: A Flutter plugin for AppsFlyer SDK. Supports iOS and Android. -version: 6.15.2 +version: 6.16.2 homepage: https://github.com/AppsFlyerSDK/flutter_appsflyer_sdk From 49c64e102afc907e8f4f857b073e425568b18811 Mon Sep 17 00:00:00 2001 From: Dani Koza <103039399+Dani-Koza-AF@users.noreply.github.com> Date: Thu, 26 Jun 2025 13:15:07 +0300 Subject: [PATCH 24/42] fixed Locale issue by forcing toUpperCase(Locale.ENGLISH) (#395) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Expanded the unit–tests to verify not only that the right native method is invoked, but also that the correct arguments are passed. --- .../appsflyersdk/AppsflyerSdkPlugin.java | 3 +- example/lib/main_page.dart | 2 +- test/appsflyer_sdk_test.dart | 61 +++++++++++++++---- 3 files changed, 51 insertions(+), 15 deletions(-) diff --git a/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java b/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java index 5c053485..23eb7978 100644 --- a/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java +++ b/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java @@ -34,6 +34,7 @@ import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Locale; import java.util.Map; import io.flutter.embedding.engine.plugins.FlutterPlugin; @@ -1008,7 +1009,7 @@ private void logAdRevenue(MethodCall call, Result result) { double revenue = requireNonNullArgument(call, "revenue"); String mediationNetworkString = requireNonNullArgument(call, "mediationNetwork"); - MediationNetwork mediationNetwork = MediationNetwork.valueOf(mediationNetworkString.toUpperCase()); + MediationNetwork mediationNetwork = MediationNetwork.valueOf(mediationNetworkString.toUpperCase(Locale.ENGLISH)); // No null check for additionalParameters since it's acceptable for it to be null (optional data) Map additionalParameters = call.argument("additionalParameters"); diff --git a/example/lib/main_page.dart b/example/lib/main_page.dart index 70ce33b7..bb55c544 100644 --- a/example/lib/main_page.dart +++ b/example/lib/main_page.dart @@ -169,7 +169,7 @@ class MainPageState extends State { AdRevenueData adRevenueData = AdRevenueData( monetizationNetwork: 'SpongeBob', - mediationNetwork: AFMediationNetwork.googleAdMob.value, + mediationNetwork: AFMediationNetwork.applovinMax.value, currencyIso4217Code: 'USD', revenue: 100.3, additionalParameters: customParams); diff --git a/test/appsflyer_sdk_test.dart b/test/appsflyer_sdk_test.dart index 3b25f472..4997fe60 100644 --- a/test/appsflyer_sdk_test.dart +++ b/test/appsflyer_sdk_test.dart @@ -7,6 +7,7 @@ void main() { late AppsflyerSdk instance; String selectedMethod = ""; + dynamic capturedArguments; const MethodChannel methodChannel = MethodChannel('af-api'); const MethodChannel callbacksChannel = MethodChannel('callbacks'); const EventChannel eventChannel = EventChannel('af-events'); @@ -63,6 +64,7 @@ void main() { case 'disableSKAdNetwork': case 'setDisableAdvertisingIdentifiers': selectedMethod = method; + capturedArguments = methodCall.arguments; break; } return null; @@ -73,9 +75,18 @@ void main() { String method = methodCall.method; if (method == 'listen') { selectedMethod = method; + capturedArguments = methodCall.arguments; } return null; }); + + // Mock handler for callbacks channel to avoid MissingPluginException during startListening + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(callbacksChannel, (methodCall) async { + selectedMethod = methodCall.method; + capturedArguments = methodCall.arguments; + return null; + }); }); test('check initSdk call', () async { @@ -90,11 +101,14 @@ void main() { group('AppsFlyerSdk', () { setUp(() { selectedMethod = ""; + capturedArguments = null; }); tearDown(() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(methodChannel, null); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(callbacksChannel, null); }); test('check logEvent call', () async { @@ -104,45 +118,56 @@ void main() { }); test('check setHost call', () async { - instance.setHost("", ""); + instance.setHost("prefix", "hostname"); expect(selectedMethod, 'setHost'); + expect(capturedArguments['hostPrefix'], 'prefix'); + expect(capturedArguments['hostName'], 'hostname'); }); test('check setCurrencyCode call', () async { - instance.setCurrencyCode("currencyCode"); + instance.setCurrencyCode("USD"); expect(selectedMethod, 'setCurrencyCode'); + expect(capturedArguments['currencyCode'], 'USD'); }); test('check setIsUpdate call', () async { instance.setIsUpdate(true); expect(selectedMethod, 'setIsUpdate'); + expect(capturedArguments['isUpdate'], true); }); test('check stop call', () async { instance.stop(true); expect(selectedMethod, 'stop'); + expect(capturedArguments['isStopped'], true); }); test('check updateServerUninstallToken call', () async { - instance.updateServerUninstallToken("token"); + instance.updateServerUninstallToken("token123"); expect(selectedMethod, 'updateServerUninstallToken'); + expect(capturedArguments['token'], 'token123'); }); test('check setOneLinkCustomDomain call', () async { instance.setOneLinkCustomDomain(["brandDomains"]); expect(selectedMethod, 'setOneLinkCustomDomain'); + expect(capturedArguments, isA()); + expect(capturedArguments, contains('brandDomains')); }); test('check logCrossPromotionAndOpenStore call', () async { - instance.logCrossPromotionAndOpenStore("appId", "campaign", null); + instance.logCrossPromotionAndOpenStore("appId123", "campaignA", null); expect(selectedMethod, 'logCrossPromotionAndOpenStore'); + expect(capturedArguments['appId'], 'appId123'); + expect(capturedArguments['campaign'], 'campaignA'); + expect(capturedArguments['params'], null); }); test('check logCrossPromotionImpression call', () async { @@ -177,9 +202,12 @@ void main() { test('check validateAndLogInAppPurchase call', () async { instance.validateAndLogInAppAndroidPurchase( - "publicKey", "signature", "purchaseData", "price", "currency", null); + "publicKey", "signature", "purchaseData", "9.99", "EUR", null); expect(selectedMethod, 'validateAndLogInAppAndroidPurchase'); + expect(capturedArguments['publicKey'], 'publicKey'); + expect(capturedArguments['price'], '9.99'); + expect(capturedArguments['currency'], 'EUR'); }); test('check setMinTimeBetweenSessions call', () async { @@ -213,9 +241,13 @@ void main() { }); test('check setUserEmails call', () async { - instance.setUserEmails(["emails"], EmailCryptType.EmailCryptTypeNone); + instance.setUserEmails( + ["user@example.com"], EmailCryptType.EmailCryptTypeSHA256); expect(selectedMethod, 'setUserEmails'); + expect(capturedArguments['emails'], contains('user@example.com')); + expect(capturedArguments['cryptType'], + EmailCryptType.values.indexOf(EmailCryptType.EmailCryptTypeSHA256)); }); test('check setAdditionalData call', () async { @@ -262,17 +294,14 @@ void main() { test('check logAdRevenue call', () async { final adRevenueData = AdRevenueData( - monetizationNetwork: 'GoogleAdMob', - mediationNetwork: AFMediationNetwork.googleAdMob.value, + monetizationNetwork: 'Applovin', + mediationNetwork: AFMediationNetwork.applovinMax.value, currencyIso4217Code: 'USD', - revenue: 1.23, - additionalParameters: { - 'adUnitId': 'ca-app-pub-XXXX/YYYY', - 'ad_network_click_id': '12345' - }); + revenue: 0.99); instance.logAdRevenue(adRevenueData); expect(selectedMethod, 'logAdRevenue'); + expect(capturedArguments['mediationNetwork'], 'applovin_max'); }); test('check setConsentData call', () async { @@ -301,12 +330,15 @@ void main() { instance.setPartnerData('partnerId', {'key': 'value'}); expect(selectedMethod, 'setPartnerData'); + expect(capturedArguments['partnerId'], 'partnerId'); + expect(capturedArguments['partnersData']['key'], 'value'); }); test('check setResolveDeepLinkURLs call', () async { instance.setResolveDeepLinkURLs(['https://example.com']); expect(selectedMethod, 'setResolveDeepLinkURLs'); + expect(capturedArguments, contains('https://example.com')); }); test('check setPushNotification call', () async { @@ -319,12 +351,14 @@ void main() { instance.sendPushNotificationData({'key': 'value'}); expect(selectedMethod, 'sendPushNotificationData'); + expect(capturedArguments['key'], 'value'); }); test('check enableFacebookDeferredApplinks call', () async { instance.enableFacebookDeferredApplinks(true); expect(selectedMethod, 'enableFacebookDeferredApplinks'); + expect(capturedArguments['isFacebookDeferredApplinksEnabled'], true); }); test('check disableSKAdNetwork call', () async { @@ -337,6 +371,7 @@ void main() { instance.setDisableAdvertisingIdentifiers(true); expect(selectedMethod, 'setDisableAdvertisingIdentifiers'); + expect(capturedArguments, true); }); }); } From 83789ce327c03bb46c6a175bdd946735b15e0712 Mon Sep 17 00:00:00 2001 From: Dani Koza <103039399+Dani-Koza-AF@users.noreply.github.com> Date: Mon, 14 Jul 2025 11:14:11 +0300 Subject: [PATCH 25/42] Doc fix (#400) doc fix - broken link --- CHANGELOG.md | 3 +++ doc/API.md | 2 +- lib/src/appsflyer_constants.dart | 2 +- pubspec.yaml | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc9a2d85..d6ccaebf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,7 @@ # Versions +## 6.16.21 +- Bug fix for users who reported Locale issue on Android, fixed Locale issue by forcing toUpperCase(Locale.ENGLISH) +- Expanded the unit–tests ## 6.16.2 - setConsentData is now deprecated! - setConsentDataV2 is the new and recommended way to set manual user consent. diff --git a/doc/API.md b/doc/API.md index 1b6327d1..9861fe06 100644 --- a/doc/API.md +++ b/doc/API.md @@ -224,7 +224,7 @@ _Example:_ | parameter | type | description | | ------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `eventName` | `String` | custom event name, is presented in your dashboard. See the Event list [HERE](https://github.com/AppsFlyerSDK/cordova-plugin-appsflyer-sdk/blob/master/src/ios/AppsFlyerTracker.h) | +| `eventName` | `String` | Use descriptive, action-based names (e.g., "purchase", "add_to_cart", "level_completed"), keep names concise but meaningful, use lowercase with underscores for consistency and avoid special characters and spaces. See the [recommended event list by business](https://support.appsflyer.com/hc/en-us/articles/115005544169-In-app-events-Overview#recommended-events-by-business-vertical). | | `eventValues` | `Map` | event details | _Example:_ diff --git a/lib/src/appsflyer_constants.dart b/lib/src/appsflyer_constants.dart index cd1c447e..fe67ec2b 100644 --- a/lib/src/appsflyer_constants.dart +++ b/lib/src/appsflyer_constants.dart @@ -3,7 +3,7 @@ part of appsflyer_sdk; enum EmailCryptType { EmailCryptTypeNone, EmailCryptTypeSHA256 } class AppsflyerConstants { - static const String PLUGIN_VERSION = "6.16.2"; + static const String PLUGIN_VERSION = "6.16.21"; static const String AF_DEV_KEY = "afDevKey"; static const String AF_APP_Id = "afAppId"; static const String AF_IS_DEBUG = "isDebug"; diff --git a/pubspec.yaml b/pubspec.yaml index 9645e263..2c67b59d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: appsflyer_sdk description: A Flutter plugin for AppsFlyer SDK. Supports iOS and Android. -version: 6.16.2 +version: 6.16.21 homepage: https://github.com/AppsFlyerSDK/flutter_appsflyer_sdk From 5f04fa3d361ca161b083f5fd79e3257cab9fd684 Mon Sep 17 00:00:00 2001 From: Dani Koza <103039399+Dani-Koza-AF@users.noreply.github.com> Date: Sun, 27 Jul 2025 15:02:55 +0300 Subject: [PATCH 26/42] Add purchase connector to development branch (#402) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * removed duplicated declaration * Update .gitignore for purchase connector feature * Add complete Purchase Connector implementation - Add Purchase Connector support for Android and iOS platforms - Implement conditional compilation with include/exclude source sets - Add comprehensive Dart API with type-safe models - Include platform-specific error handling and validation - Add Purchase Connector documentation - Support for both in-app purchases and subscriptions - Zero impact when disabled via gradle/podfile flags New files: - Complete lib/src/purchase_connector/ Dart implementation - Android: include/exclude-connector source sets with ConnectorWrapper - iOS: PurchaseConnectorPlugin.swift with conditional compilation - Documentation: PurchaseConnector.md Modified integration points: - Android: build.gradle, AppsflyerSdkPlugin.java - iOS: appsflyer_sdk.podspec with subspecs architecture - Flutter: appsflyer_sdk.dart main export file * Complete Purchase Connector integration with code generation - Add missing Purchase Connector constants and AFMediationNetwork enum - Update pubspec.yaml with required dependencies (json_annotation, build_runner, json_serializable) - Generate JSON serialization code for all Purchase Connector models - Fix all compilation errors and undefined references - All 39 tests passing ✅ - Purchase Connector fully functional with type-safe models Generated files: - lib/appsflyer_sdk.g.dart - JSON serialization support - All Purchase Connector model serialization methods Dependencies added: - json_annotation: ^4.9.0 (already present) - build_runner: ^2.3.0 - json_serializable: ^6.5.4 * setting the proper SDK versions * documentation small fix * restore and solve code conflicts and ghost code Aligned with development --- .gitignore | 3 +- android/build.gradle | 22 +- .../AppsFlyerPurchaseConnector.kt | 8 + .../AppsFlyerPurchaseConnector.kt | 207 ++++++++ .../appsflyersdk/ConnectorWrapper.kt | 251 ++++++++++ .../appsflyersdk/AppsflyerSdkPlugin.java | 7 +- doc/PurchaseConnector.md | 323 ++++++++++++ .../PurchaseConnectorPlugin.swift | 170 +++++++ ios/appsflyer_sdk.podspec | 36 +- lib/appsflyer_sdk.dart | 20 +- lib/appsflyer_sdk.g.dart | 461 ++++++++++++++++++ lib/src/appsflyer_constants.dart | 32 +- .../connector_callbacks.dart | 18 + .../missing_configuration_exception.dart | 11 + .../in_app_purchase_validation_result.dart | 33 ++ .../purchase_connector/models/ios_error.dart | 15 + .../models/jvm_throwable.dart | 16 + .../models/product_purchase.dart | 44 ++ .../models/subscription_purchase.dart | 343 +++++++++++++ .../subscription_validation_result.dart | 32 ++ .../models/validation_failure_data.dart | 20 + .../purchase_connector.dart | 254 ++++++++++ .../purchase_connector_configuration.dart | 16 + pubspec.yaml | 5 +- 24 files changed, 2322 insertions(+), 25 deletions(-) create mode 100644 android/src/main/exlude-connector/com/appsflyer/appsflyersdk/AppsFlyerPurchaseConnector.kt create mode 100644 android/src/main/include-connector/com/appsflyer/appsflyersdk/AppsFlyerPurchaseConnector.kt create mode 100644 android/src/main/include-connector/com/appsflyer/appsflyersdk/ConnectorWrapper.kt create mode 100644 doc/PurchaseConnector.md create mode 100644 ios/PurchaseConnector/PurchaseConnectorPlugin.swift create mode 100644 lib/appsflyer_sdk.g.dart create mode 100644 lib/src/purchase_connector/connector_callbacks.dart create mode 100644 lib/src/purchase_connector/missing_configuration_exception.dart create mode 100644 lib/src/purchase_connector/models/in_app_purchase_validation_result.dart create mode 100644 lib/src/purchase_connector/models/ios_error.dart create mode 100644 lib/src/purchase_connector/models/jvm_throwable.dart create mode 100644 lib/src/purchase_connector/models/product_purchase.dart create mode 100644 lib/src/purchase_connector/models/subscription_purchase.dart create mode 100644 lib/src/purchase_connector/models/subscription_validation_result.dart create mode 100644 lib/src/purchase_connector/models/validation_failure_data.dart create mode 100644 lib/src/purchase_connector/purchase_connector.dart create mode 100644 lib/src/purchase_connector/purchase_connector_configuration.dart diff --git a/.gitignore b/.gitignore index 591007ae..db41b259 100644 --- a/.gitignore +++ b/.gitignore @@ -107,10 +107,11 @@ example/web/* example/android/app/.cxx +example/android/app/.cxx/* + example/\.metadata example/analysis_options.yaml - node_modules/ covBadgeGen.js diff --git a/android/build.gradle b/android/build.gradle index fc69f9fc..fb0b2b21 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -16,6 +16,8 @@ rootProject.allprojects { apply plugin: 'com.android.library' apply plugin: 'org.jetbrains.kotlin.android' +def includeConnector = project.findProperty('appsflyer.enable_purchase_connector')?.toBoolean() ?: false + android { defaultConfig { minSdkVersion 19 @@ -24,25 +26,37 @@ android { multiDexEnabled true } + lintOptions { disable 'InvalidPackage' } namespace 'com.appsflyer.appsflyersdk' + sourceSets { + main { + java.srcDirs = ['src/main/java'] + java.srcDirs += includeConnector ? ['src/main/include-connector'] : ['src/main/exlude-connector'] + } + includeConnector ? ['src/main/include-connector'] : ['src/main/exlude-connector'] + } + compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } - + kotlinOptions { jvmTarget = '17' } - } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.appcompat:appcompat:1.0.0' - implementation 'com.appsflyer:af-android-sdk:6.16.2' - implementation 'com.android.installreferrer:installreferrer:2.1' + implementation 'com.appsflyer:af-android-sdk:6.17.0' + implementation 'com.android.installreferrer:installreferrer:2.2' +// implementation 'androidx.core:core-ktx:1.13.1' + if (includeConnector) { + implementation 'com.appsflyer:purchase-connector:2.1.1' + } } \ No newline at end of file diff --git a/android/src/main/exlude-connector/com/appsflyer/appsflyersdk/AppsFlyerPurchaseConnector.kt b/android/src/main/exlude-connector/com/appsflyer/appsflyersdk/AppsFlyerPurchaseConnector.kt new file mode 100644 index 00000000..188033f0 --- /dev/null +++ b/android/src/main/exlude-connector/com/appsflyer/appsflyersdk/AppsFlyerPurchaseConnector.kt @@ -0,0 +1,8 @@ +package com.appsflyer.appsflyersdk + +import io.flutter.embedding.engine.plugins.FlutterPlugin + +object AppsFlyerPurchaseConnector: FlutterPlugin { + override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) = Unit + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) = Unit +} \ No newline at end of file diff --git a/android/src/main/include-connector/com/appsflyer/appsflyersdk/AppsFlyerPurchaseConnector.kt b/android/src/main/include-connector/com/appsflyer/appsflyersdk/AppsFlyerPurchaseConnector.kt new file mode 100644 index 00000000..a923fe9e --- /dev/null +++ b/android/src/main/include-connector/com/appsflyer/appsflyersdk/AppsFlyerPurchaseConnector.kt @@ -0,0 +1,207 @@ +package com.appsflyer.appsflyersdk + +import android.content.Context +import android.os.Handler +import android.os.Looper +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import org.json.JSONObject +import java.lang.ref.WeakReference + + +/** + * A Flutter plugin that establishes a bridge between the Flutter appsflyer SDK and the Native Android Purchase Connector. + * + * This plugin utilizes MethodChannels to communicate between Flutter and native Android, + * passing method calls and event callbacks. + * + * @property methodChannel used to set up the communication channel between Flutter and Android. + * @property contextRef a Weak Reference to the application context when the plugin is first attached. Used to build the Appsflyer's Purchase Connector. + * @property connectorWrapper wraps the Appsflyer's Android purchase client and bridge map conversion methods. Used to perform various operations (configure, start/stop observing transactions). + * @property arsListener an object of [MappedValidationResultListener] that handles SubscriptionPurchaseValidationResultListener responses and failures. Lazily initialized. + * @property viapListener an object of [MappedValidationResultListener] that handles InAppValidationResultListener responses and failures. Lazily initialized. + */ +object AppsFlyerPurchaseConnector : FlutterPlugin, MethodChannel.MethodCallHandler { + private var methodChannel: MethodChannel? = null + private var contextRef: WeakReference? = null + private var connectorWrapper: ConnectorWrapper? = null + private val handler by lazy { Handler(Looper.getMainLooper()) } + + private val arsListener: MappedValidationResultListener by lazy { + object : MappedValidationResultListener { + override fun onFailure(result: String, error: Throwable?) { + val resMap = mapOf("result" to result, "error" to error?.toMap()) + methodChannel?.invokeMethodOnUI( + "SubscriptionPurchaseValidationResultListener:onFailure", + resMap + ) + } + + override fun onResponse(p0: Map?) { + methodChannel?.invokeMethodOnUI( + "SubscriptionPurchaseValidationResultListener:onResponse", + p0 + ) + } + } + } + + private val viapListener: MappedValidationResultListener by lazy { + object : MappedValidationResultListener { + override fun onFailure(result: String, error: Throwable?) { + val resMap = mapOf("result" to result, "error" to error?.toMap()) + methodChannel?.invokeMethodOnUI("InAppValidationResultListener:onFailure", resMap) + } + + override fun onResponse(p0: Map?) { + methodChannel?.invokeMethodOnUI("InAppValidationResultListener:onResponse", p0) + } + } + } + + private fun MethodChannel?.invokeMethodOnUI(method: String, args: Any?) = this?.let { + handler.post { + val data = if (args is Map<*, *>) { + JSONObject(args).toString() + } else { + args + } + it.invokeMethod(method, data) + } + } + + + /** + * Called when the plugin is attached to the Flutter engine. + * + * It sets up the MethodChannel and retains the application context. + * + * @param binding The binding provides access to the binary messenger and application context. + */ + override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { + methodChannel = + MethodChannel( + binding.binaryMessenger, + AppsFlyerConstants.AF_PURCHASE_CONNECTOR_CHANNEL + ).also { + it.setMethodCallHandler(this) + } + contextRef = WeakReference(binding.applicationContext) + } + + /** + * Called when the plugin is detached from the Flutter engine. + * + * @param binding The binding that was provided in [onAttachedToEngine]. + */ + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) = Unit + + /** + * Handles incoming method calls from Flutter. + * + * It either triggers a connector operation or returns an unimplemented error. + * Supported operations are configuring, starting and stopping observing transactions. + * + * @param call The method call from Flutter. + * @param result The result to be returned to Flutter. + */ + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + when (call.method) { + "startObservingTransactions" -> startObservingTransactions(result) + "stopObservingTransactions" -> stopObservingTransactions(result) + "configure" -> configure(call, result) + else -> result.notImplemented() + } + } + + /** + * Configures the purchase connector with the parameters sent from Flutter. + * + * @param call The method call from Flutter. + * @param result The result to be returned to Flutter. + */ + private fun configure(call: MethodCall, result: MethodChannel.Result) { + if (connectorWrapper == null) { + contextRef?.get()?.let { ctx -> + connectorWrapper = ConnectorWrapper( + ctx, call.getBoolean("logSubscriptions"), + call.getBoolean("logInApps"), + call.getBoolean("sandbox"), + arsListener, viapListener + ) + result.success(null) + } ?: run { + result.error("402", "Missing context. Is plugin attached to engine?", null) + } + + } else { + result.error("401", "Connector already configured", null) + } + } + + /** + * Starts observing transactions. + * + * @param result The result to be returned to Flutter. + */ + private fun startObservingTransactions(result: MethodChannel.Result) = + connectorOperation(result) { + it.startObservingTransactions() + } + + /** + * Stops observing transactions. + * + * @param result The result to be returned to Flutter. + */ + private fun stopObservingTransactions(result: MethodChannel.Result) = + connectorOperation(result) { + it.stopObservingTransactions() + } + + /** + * Performs a specified operation on the connector after confirming that the connector has been configured. + * + * @param result The result to be returned to Flutter. + * @param exc The operation to be performed on the connector. + */ + private fun connectorOperation( + result: MethodChannel.Result, + exc: (connectorWrapper: ConnectorWrapper) -> Unit + ) { + if (connectorWrapper != null) { + exc(connectorWrapper!!) + result.success(null) + } else { + result.error("404", "Connector not configured, did you called `configure` first?", null) + } + } + + /** + * Converts a [Throwable] to a Map that can be returned to Flutter. + * + * @return A map representing the [Throwable]. + */ + private fun Throwable.toMap(): Map { + return mapOf( + "type" to this::class.simpleName, + "message" to this.message, + "stacktrace" to this.stackTrace.joinToString(separator = "\n") { it.toString() }, + "cause" to this.cause?.toMap() + ) + } + + /** + * Attempts to get a Boolean argument from the method call. + * + * If unsuccessful, it returns the default value. + * + * @param key The key for the argument. + * @param defValue The default value to be returned if the argument does not exist. + * @return The value of the argument or the default value if the argument does not exist. + */ + private fun MethodCall.getBoolean(key: String, defValue: Boolean = false): Boolean = + runCatching { argument(key)!! }.getOrDefault(defValue) + +} \ No newline at end of file diff --git a/android/src/main/include-connector/com/appsflyer/appsflyersdk/ConnectorWrapper.kt b/android/src/main/include-connector/com/appsflyer/appsflyersdk/ConnectorWrapper.kt new file mode 100644 index 00000000..cc9e2bfc --- /dev/null +++ b/android/src/main/include-connector/com/appsflyer/appsflyersdk/ConnectorWrapper.kt @@ -0,0 +1,251 @@ +package com.appsflyer.appsflyersdk + +import android.content.Context +import com.appsflyer.api.PurchaseClient +import com.appsflyer.api.Store +import com.appsflyer.internal.models.* +import com.appsflyer.internal.models.InAppPurchaseValidationResult +import com.appsflyer.internal.models.SubscriptionPurchase +import com.appsflyer.internal.models.SubscriptionValidationResult +import com.appsflyer.internal.models.ValidationFailureData + +interface MappedValidationResultListener : PurchaseClient.ValidationResultListener> + +/** + * A connector class that wraps the Android purchase connector client. + * + * This class uses the Builder pattern to configure the Android purchase connector client. + * It implements the [PurchaseClient] interface required by the appsflyer_sdk and translates + * the various callbacks and responses between the two interfaces. + * + * @property context The application context. + * @property logSubs If true, subscription transactions will be logged. + * @property logInApps If true, in-app purchase transactions will be logged. + * @property sandbox If true, the purchase client will be in sandbox mode. + * @property subsListener The listener for subscription purchase validation results. + * @property inAppListener The listener for in-app purchase validation Result. + */ +class ConnectorWrapper( + context: Context, + logSubs: Boolean, + logInApps: Boolean, + sandbox: Boolean, + subsListener: MappedValidationResultListener, + inAppListener: MappedValidationResultListener, +) : + PurchaseClient { + private val connector = + PurchaseClient.Builder(context, Store.GOOGLE).setSandbox(sandbox).logSubscriptions(logSubs) + .autoLogInApps(logInApps).setSubscriptionValidationResultListener(object : + PurchaseClient.SubscriptionPurchaseValidationResultListener { + override fun onResponse(result: Map?) { + subsListener.onResponse(result?.entries?.associate { (k, v) -> k to v.toJsonMap() }) + } + + override fun onFailure(result: String, error: Throwable?) { + subsListener.onFailure(result, error) + } + }).setInAppValidationResultListener(object : PurchaseClient.InAppPurchaseValidationResultListener{ + override fun onResponse(result: Map?) { + inAppListener.onResponse(result?.entries?.associate { (k, v) -> k to v.toJsonMap() }) + } + + override fun onFailure(result: String, error: Throwable?) { + inAppListener.onFailure(result, error) + } + }) + .build() + + /** + * Starts observing all incoming transactions from the play store. + */ + override fun startObservingTransactions() = connector.startObservingTransactions() + + /** + * Stops observing all incoming transactions from the play store. + */ + override fun stopObservingTransactions() = connector.stopObservingTransactions() + + + /** + * Converts [SubscriptionPurchase] to a Json map, which then is delivered to SDK's method response. + * + * @return A map representing this SubscriptionPurchase. + */ + private fun SubscriptionPurchase.toJsonMap(): Map { + return mapOf( + "acknowledgementState" to acknowledgementState, + "canceledStateContext" to canceledStateContext?.toJsonMap(), + "externalAccountIdentifiers" to externalAccountIdentifiers?.toJsonMap(), + "kind" to kind, + "latestOrderId" to latestOrderId, + "lineItems" to lineItems.map { it.toJsonMap() }, + "linkedPurchaseToken" to linkedPurchaseToken, + "pausedStateContext" to pausedStateContext?.toJsonMap(), + "regionCode" to regionCode, + "startTime" to startTime, + "subscribeWithGoogleInfo" to subscribeWithGoogleInfo?.toJsonMap(), + "subscriptionState" to subscriptionState, + "testPurchase" to testPurchase?.toJsonMap() + ) + } + + private fun CanceledStateContext.toJsonMap(): Map { + return mapOf( + "developerInitiatedCancellation" to developerInitiatedCancellation?.toJsonMap(), + "replacementCancellation" to replacementCancellation?.toJsonMap(), + "systemInitiatedCancellation" to systemInitiatedCancellation?.toJsonMap(), + "userInitiatedCancellation" to userInitiatedCancellation?.toJsonMap() + ) + } + + private fun DeveloperInitiatedCancellation.toJsonMap(): Map { + return mapOf() + } + + private fun ReplacementCancellation.toJsonMap(): Map { + return mapOf() + } + + private fun SystemInitiatedCancellation.toJsonMap(): Map { + return mapOf() + } + + private fun UserInitiatedCancellation.toJsonMap(): Map { + return mapOf( + "cancelSurveyResult" to cancelSurveyResult?.toJsonMap(), + "cancelTime" to cancelTime + ) + } + + private fun CancelSurveyResult.toJsonMap(): Map { + return mapOf( + "reason" to reason, + "reasonUserInput" to reasonUserInput + ) + } + + private fun ExternalAccountIdentifiers.toJsonMap(): Map { + return mapOf( + "externalAccountId" to externalAccountId, + "obfuscatedExternalAccountId" to obfuscatedExternalAccountId, + "obfuscatedExternalProfileId" to obfuscatedExternalProfileId + ) + } + + private fun SubscriptionPurchaseLineItem.toJsonMap(): Map { + return mapOf( + "autoRenewingPlan" to autoRenewingPlan?.toJsonMap(), + "deferredItemReplacement" to deferredItemReplacement?.toJsonMap(), + "expiryTime" to expiryTime, + "offerDetails" to offerDetails?.toJsonMap(), + "prepaidPlan" to prepaidPlan?.toJsonMap(), + "productId" to productId + ) + } + + private fun OfferDetails.toJsonMap(): Map { + return mapOf( + "offerTags" to offerTags, + "basePlanId" to basePlanId, + "offerId" to offerId + ) + } + + private fun AutoRenewingPlan.toJsonMap(): Map { + return mapOf( + "autoRenewEnabled" to autoRenewEnabled, + "priceChangeDetails" to priceChangeDetails?.toJsonMap() + ) + } + + private fun SubscriptionItemPriceChangeDetails.toJsonMap(): Map { + return mapOf( + "expectedNewPriceChargeTime" to expectedNewPriceChargeTime, + "newPrice" to newPrice?.toJsonMap(), + "priceChangeMode" to priceChangeMode, + "priceChangeState" to priceChangeState + ) + } + + private fun Money.toJsonMap(): Map { + return mapOf( + "currencyCode" to currencyCode, + "nanos" to nanos, + "units" to units + ) + } + + private fun DeferredItemReplacement.toJsonMap(): Map { + return mapOf("productId" to productId) + } + + private fun PrepaidPlan.toJsonMap(): Map { + return mapOf("allowExtendAfterTime" to allowExtendAfterTime) + } + + private fun PausedStateContext.toJsonMap(): Map { + return mapOf("autoResumeTime" to autoResumeTime) + } + + private fun SubscribeWithGoogleInfo.toJsonMap(): Map { + return mapOf( + "emailAddress" to emailAddress, + "familyName" to familyName, + "givenName" to givenName, + "profileId" to profileId, + "profileName" to profileName + ) + } + + fun TestPurchase.toJsonMap(): Map { + return mapOf() + } + + private fun ProductPurchase.toJsonMap(): Map { + return mapOf( + "kind" to kind, + "purchaseTimeMillis" to purchaseTimeMillis, + "purchaseState" to purchaseState, + "consumptionState" to consumptionState, + "developerPayload" to developerPayload, + "orderId" to orderId, + "purchaseType" to purchaseType, + "acknowledgementState" to acknowledgementState, + "purchaseToken" to purchaseToken, + "productId" to productId, + "quantity" to quantity, + "obfuscatedExternalAccountId" to obfuscatedExternalAccountId, + "obfuscatedExternalProfileId" to obfuscatedExternalProfileId, + "regionCode" to regionCode + ) + } + + /** + * Converts [InAppPurchaseValidationResult] into a map of objects so that the Object can be passed to Flutter using a method channel + * + * @return A map representing this InAppPurchaseValidationResult. + */ + private fun InAppPurchaseValidationResult.toJsonMap(): Map { + return mapOf( + "success" to success, + "productPurchase" to productPurchase?.toJsonMap(), + "failureData" to failureData?.toJsonMap() + ) + } + + private fun SubscriptionValidationResult.toJsonMap(): Map { + return mapOf( + "success" to success, + "subscriptionPurchase" to subscriptionPurchase?.toJsonMap(), + "failureData" to failureData?.toJsonMap() + ) + } + + private fun ValidationFailureData.toJsonMap(): Map { + return mapOf( + "status" to status, + "description" to description + ) + } +} \ No newline at end of file diff --git a/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java b/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java index 23eb7978..a5245f7a 100644 --- a/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java +++ b/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java @@ -237,7 +237,7 @@ public void onMethodCall(MethodCall call, Result result) { break; case "setConsentDataV2": setConsentDataV2(call, result); - break; + break; case "setIsUpdate": setIsUpdate(call, result); break; @@ -431,7 +431,7 @@ private void startSDK(MethodCall call, final Result result) { /** * Sets the user consent data for tracking. - * @deprecated Use {@link #setConsentDataV2(MethodCall, Result)} instead. + * @deprecated Use {@link #setConsentDataV2(MethodCall, Result)} instead! */ @Deprecated public void setConsentData(MethodCall call, Result result) { @@ -1106,6 +1106,7 @@ private Map replaceNullValues(Map map) { @Override public void onAttachedToEngine(FlutterPluginBinding binding) { onAttachedToEngine(binding.getApplicationContext(), binding.getBinaryMessenger()); + AppsFlyerPurchaseConnector.INSTANCE.onAttachedToEngine(binding); } @Override @@ -1114,6 +1115,7 @@ public void onDetachedFromEngine(FlutterPluginBinding binding) { mMethodChannel = null; mEventChannel.setStreamHandler(null); mEventChannel = null; + AppsFlyerPurchaseConnector.INSTANCE.onDetachedFromEngine(binding); mContext = null; mApplication = null; } @@ -1143,4 +1145,5 @@ public void onDetachedFromActivity() { saveCallbacks = true; AppsFlyerLib.getInstance().unregisterConversionListener(); } + } diff --git a/doc/PurchaseConnector.md b/doc/PurchaseConnector.md new file mode 100644 index 00000000..e076ebee --- /dev/null +++ b/doc/PurchaseConnector.md @@ -0,0 +1,323 @@ + +# Flutter Purchase Connector +**At a glance:** Automatically validate and measure revenue from in-app purchases and auto-renewable subscriptions to get the full picture of your customers' life cycles and accurate ROAS measurements. +For more information please check the following pages: +* [ROI360 in-app purchase (IAP) and subscription revenue measurement](https://support.appsflyer.com/hc/en-us/articles/7459048170769-ROI360-in-app-purchase-IAP-and-subscription-revenue-measurement?query=purchase) +* [Android Purchase Connector](https://dev.appsflyer.com/hc/docs/purchase-connector-android) +* [iOS Purchase Connector](https://dev.appsflyer.com/hc/docs/purchase-connector-ios) + +🛠 In order for us to provide optimal support, we would kindly ask you to submit any issues to +support@appsflyer.com + +> *When submitting an issue please specify your AppsFlyer sign-up (account) email , your app ID , production steps, logs, code snippets and any additional relevant information.* + +## Table Of Content + + +* [Important Note](#important-note) +* [Adding The Connector To Your Project](#install-connector) + - [How to Opt-In](#install-connector) + - [What Happens if You Use Dart Files Without Opting In?](#install-connector) +* [Basic Integration Of The Connector](#basic-integration) + - [Create PurchaseConnector Instance](#create-instance) + - [Start Observing Transactions](#start) + - [Stop Observing Transactions](#stop) + - [Log Subscriptions](#log-subscriptions) + - [Log In App Purchases](#log-inapps) +* [Register Validation Results Listeners](#validation-callbacks) + - [Cross-Platform Considerations](#cross-platform-considerations) + - [Android Callback Types](#android-callback-types) + - [Android - Subscription Validation Result Listener](#ars-validation-callbacks) + - [Android In Apps Validation Result Listener](#inapps-validation-callbacks) + - [iOS Combined Validation Result Listener](#ios-callback) +* [Testing the Integration](#testing) + - [Android](#testing-android) + - [iOS](#testing-ios) + - [Dart Usage for Android and iOS](#testing-config) +* [ProGuard Rules for Android](#proguard) +* [Full Code Example](#example) + + + +## âš ī¸ âš ī¸ Important Note âš ī¸ âš ī¸ + +The Purchase Connector feature of the AppsFlyer SDK depends on specific libraries provided by Google and Apple for managing in-app purchases: + +- For Android, it depends on the [Google Play Billing Library](https://developer.android.com/google/play/billing/integrate) (Supported versions: 5.x.x - 7.x.x). +- For iOS, it depends on [StoreKit](https://developer.apple.com/documentation/storekit). (Supported versions are StoreKit V1 + V2) + +However, these dependencies aren't actively included with the SDK. This means that the responsibility of managing these dependencies and including the necessary libraries in your project falls on you as the consumer of the SDK. + +If you're implementing in-app purchases in your app, you'll need to ensure that the Google Play Billing Library (for Android) or StoreKit (for iOS) are included in your project. You can include these libraries manually in your native code, or you can use a third-party Flutter plugin, such as the [`in_app_purchase`](https://pub.dev/packages/in_app_purchase) plugin. + +Remember to appropriately manage these dependencies when implementing the Purchase Validation feature in your app. Failing to include the necessary libraries might result in failures when attempting to conduct in-app purchases or validate purchases. + +## Adding The Connector To Your Project + +The Purchase Connector feature in AppsFlyer SDK Flutter Plugin is an optional enhancement that you can choose to use based on your requirements. This feature is not included by default and you'll have to opt-in if you wish to use it. + +### How to Opt-In + +To opt-in and include this feature in your app, you need to set specific properties based on your platform: + +For **iOS**, in your Podfile located within the `iOS` folder of your Flutter project, set `$AppsFlyerPurchaseConnector` to `true`. +```ruby +$AppsFlyerPurchaseConnector = true +``` +For **Android**, in your `gradle.properties` file located within the `Android` folder of your Flutter project,, set `appsflyer.enable_purchase_connector` to `true`. +```groovy +appsflyer.enable_purchase_connector=true +``` +Once you set these properties, the Purchase Validation feature will be integrated into your project and you can utilize its functionality in your app. + +### What Happens if You Use Dart Files Without Opting In? + +The Dart files for the Purchase Validation feature are always included in the plugin. If you try to use these Dart APIs without opting into the feature, the APIs will not have effect because the corresponding native code necessary for them to function will not be included in your project. + +In such cases, you'll likely experience errors or exceptions when trying to use functionalities provided by the Purchase Validation feature. To avoid these issues, ensure that you opt-in to the feature if you intend to use any related APIs. + +## Basic Integration Of The Connector +### Create PurchaseConnector Instance +The `PurchaseConnector` requires a configuration object of type `PurchaseConnectorConfiguration` at instantiation time. This configuration object governs how the `PurchaseConnector` behaves in your application. + +To properly set up the configuration object, you must specify certain parameters: + +- `logSubscriptions`: If set to `true`, the connector logs all subscription events. +- `logInApps`: If set to `true`, the connector logs all in-app purchase events. +- `sandbox`: If set to `true`, transactions are tested in a sandbox environment. Be sure to set this to `false` in production. + +Here's an example usage: + +```dart +void main() { + final afPurchaseClient = PurchaseConnector( + config: PurchaseConnectorConfiguration( + logSubscriptions: true, // Enables logging of subscription events + logInApps: true, // Enables logging of in-app purchase events + sandbox: true, // Enables testing in a sandbox environment + ), + ); + + // Continue with your application logic... +} +``` + +**IMPORTANT**: The `PurchaseConnectorConfiguration` is required only the first time you instantiate `PurchaseConnector`. If you attempt to create a `PurchaseConnector` instance and no instance has been initialized yet, you must provide a `PurchaseConnectorConfiguration`. If an instance already exists, the system will ignore the configuration provided and will return the existing instance to enforce the singleton pattern. + +For example: + +```dart +void main() { + // Correct usage: Providing configuration at first instantiation + final purchaseConnector1 = PurchaseConnector( + config: PurchaseConnectorConfiguration( + logSubscriptions: true, + logInApps: true, + sandbox: true, + ), + ); + + // Additional instantiations will ignore the provided configuration + // and will return the previously created instance. + final purchaseConnector2 = PurchaseConnector( + config: PurchaseConnectorConfiguration( + logSubscriptions: false, + logInApps: false, + sandbox: false, + ), + ); + + // purchaseConnector1 and purchaseConnector2 point to the same instance + assert(purchaseConnector1 == purchaseConnector2); +} +``` + +Thus, always ensure that the initial configuration fully suits your requirements, as subsequent changes are not considered. + +Remember to set `sandbox` to `false` before releasing your app to production. If the production purchase event is sent in sandbox mode, your event won't be validated properly by AppsFlyer. +### Start Observing Transactions +Start the SDK instance to observe transactions.
+ +**âš ī¸ Please Note** +> This should be called right after calling the `AppsflyerSdk` [start](https://github.com/AppsFlyerSDK/appsflyer-flutter-plugin/blob/master/doc/BasicIntegration.md#startsdk). +> Calling `startObservingTransactions` activates a listener that automatically observes new billing transactions. This includes new and existing subscriptions and new in app purchases. +> The best practice is to activate the listener as early as possible. +```dart + // start + afPurchaseClient.startObservingTransactions(); +``` + +###
Stop Observing Transactions +Stop the SDK instance from observing transactions.
+**âš ī¸ Please Note** +> This should be called if you would like to stop the Connector from listening to billing transactions. This removes the listener and stops observing new transactions. +> An example for using this API is if the app wishes to stop sending data to AppsFlyer due to changes in the user's consent (opt-out from data sharing). Otherwise, there is no reason to call this method. +> If you do decide to use it, it should be called right before calling the Android SDK's [`stop`](https://dev.appsflyer.com/hc/docs/android-sdk-reference-appsflyerlib#stop) API + +```dart + // start + afPurchaseClient.stopObservingTransactions(); +``` + +###
Log Subscriptions +Enables automatic logging of subscription events.
+Set true to enable, false to disable.
+If this field is not used, by default, the connector will not record Subscriptions.
+```dart +final afPurchaseClient = PurchaseConnector( + config: PurchaseConnectorConfiguration(logSubscriptions: true)); +``` + +###
Log In App Purchases +Enables automatic logging of In-App purchase events
+Set true to enable, false to disable.
+If this field is not used, by default, the connector will not record In App Purchases.
+ +```dart +final afPurchaseClient = PurchaseConnector( + config: PurchaseConnectorConfiguration(logInApps: true)); +``` + + +##
Register Validation Results Listeners +You can register listeners to get the validation results once getting a response from AppsFlyer servers to let you know if the purchase was validated successfully.
+ +###
Cross-Platform Considerations + +The AppsFlyer SDK Flutter plugin acts as a bridge between your Flutter app and the underlying native SDKs provided by AppsFlyer. It's crucial to understand that the native infrastructure of iOS and Android is quite different, and so is the AppsFlyer SDK built on top of them. These differences are reflected in how you would handle callbacks separately for each platform. + +In the iOS environment, there is a single callback method `didReceivePurchaseRevenueValidationInfo` to handle both subscriptions and in-app purchases. You set this callback using `setDidReceivePurchaseRevenueValidationInfo`. + +On the other hand, Android segregates callbacks for subscriptions and in-app purchases. It provides two separate listener methods - `setSubscriptionValidationResultListener` for subscriptions and `setInAppValidationResultListener` for in-app purchases. These listener methods register callback handlers for `OnResponse` (executed when a successful response is received) and `OnFailure` (executed when a failure occurs, including due to a network exception or non-200/OK response from the server). + +By splitting the callbacks, you can ensure platform-specific responses and tailor your app's behavior accordingly. It's crucial to consider these nuances to ensure a smooth integration of AppsFlyer SDK into your Flutter application. + +### Android Callback Types + +| Listener Method | Description | +|-------------------------------|--------------| +| `onResponse(result: Result?)` | Invoked when we got 200 OK response from the server (INVALID purchase is considered to be successful response and will be returned to this callback) | +|`onFailure(result: String, error: Throwable?)`|Invoked when we got some network exception or non 200/OK response from the server.| + +### Android - Subscription Validation Result Listener + +```dart +// set listeners for Android +afPurchaseClient.setSubscriptionValidationResultListener( + (Map? result) { + // handle subscription validation result for Android +}, (String result, JVMThrowable? error) { + // handle subscription validation error for Android +}); +``` + +### Android In Apps Validation Result Listener +```dart +afPurchaseClient.setInAppValidationResultListener( + (Map? result) { + // handle in-app validation result for Android + }, (String result, JVMThrowable? error) { + // handle in-app validation error for Android +}); +``` + +### iOS Combined Validation Result Listener +```dart +afPurchaseClient.setDidReceivePurchaseRevenueValidationInfo((validationInfo, error) { + // handle subscription and in-app validation result and errors for iOS +}); +``` + + +## Testing the Integration + +With the AppsFlyer SDK, you can select which environment will be used for validation - either **production** or **sandbox**. By default, the environment is set to production. However, while testing your app, you should use the sandbox environment. + +### Android + +For Android, testing your integration with the [Google Play Billing Library](https://developer.android.com/google/play/billing/test) should use the sandbox environment. + +To set the environment to sandbox in Flutter, just set the `sandbox` parameter in the `PurchaseConnectorConfiguration` to `true` when instantiating `PurchaseConnector`. + +Remember to switch the environment back to production (set `sandbox` to `false`) before uploading your app to the Google Play Store. + +### iOS + +To test purchases in an iOS environment on a real device with a TestFlight sandbox account, you also need to set `sandbox` to `true`. + +> *IMPORTANT NOTE: Before releasing your app to production please be sure to set `sandbox` to `false`. If a production purchase event is sent in sandbox mode, your event will not be validated properly! * + +### Dart Usage for Android and iOS + +For both Android and iOS, you can set the sandbox environment using the `sandbox` parameter in the `PurchaseConnectorConfiguration` when you instantiate `PurchaseConnector` in your Dart code like this: + +```dart +// Testing in a sandbox environment +final purchaseConnector = PurchaseConnector( + PurchaseConnectorConfiguration(sandbox: true) +); +``` + +Remember to set `sandbox` back to `false` before releasing your app to production. If the production purchase event is sent in sandbox mode, your event won't be validated properly. + +## ProGuard Rules for Android + +If you are using ProGuard to obfuscate your APK for Android, you need to ensure that it doesn't interfere with the functionality of AppsFlyer SDK and its Purchase Connector feature. + +Add following keep rules to your `proguard-rules.pro` file: + +```groovy +-keep class com.appsflyer.** { *; } +-keep class kotlin.jvm.internal.Intrinsics{ *; } +-keep class kotlin.collections.**{ *; } +``` + +## Full Code Example +```dart +PurchaseConnectorConfiguration config = PurchaseConnectorConfiguration( + logSubscriptions: true, logInApps: true, sandbox: false); +final afPurchaseClient = PurchaseConnector(config: config); + +// set listeners for Android +afPurchaseClient.setSubscriptionValidationResultListener( + (Map? result) { + // handle subscription validation result for Android + result?.entries.forEach((element) { + debugPrint( + "Subscription Validation Result\n\t Token: ${element.key}\n\tresult: ${jsonEncode(element.value.toJson())}"); + }); +}, (String result, JVMThrowable? error) { + // handle subscription validation error for Android + var errMsg = error != null ? jsonEncode(error.toJson()) : null; + debugPrint( + "Subscription Validation Result\n\t result: $result\n\terror: $errMsg"); +}); + +afPurchaseClient.setInAppValidationResultListener( + (Map? result) { + // handle in-app validation result for Android + result?.entries.forEach((element) { + debugPrint( + "In App Validation Result\n\t Token: ${element.key}\n\tresult: ${jsonEncode(element.value.toJson())}"); + }); +}, (String result, JVMThrowable? error) { + // handle in-app validation error for Android + var errMsg = error != null ? jsonEncode(error.toJson()) : null; + debugPrint( + "In App Validation Result\n\t result: $result\n\terror: $errMsg"); +}); + +// set listener for iOS +afPurchaseClient + .setDidReceivePurchaseRevenueValidationInfo((validationInfo, error) { + var validationInfoMsg = + validationInfo != null ? jsonEncode(validationInfo) : null; + var errMsg = error != null ? jsonEncode(error.toJson()) : null; + debugPrint( + "iOS Validation Result\n\t validationInfo: $validationInfoMsg\n\terror: $errMsg"); + // handle subscription and in-app validation result and errors for iOS +}); + +// start +afPurchaseClient.startObservingTransactions(); +``` \ No newline at end of file diff --git a/ios/PurchaseConnector/PurchaseConnectorPlugin.swift b/ios/PurchaseConnector/PurchaseConnectorPlugin.swift new file mode 100644 index 00000000..dfd60f41 --- /dev/null +++ b/ios/PurchaseConnector/PurchaseConnectorPlugin.swift @@ -0,0 +1,170 @@ +// +// PurchaseConnectorPlugin.swift +// appsflyer_sdk +// +// Created by Paz Lavi on 11/06/2024. +// +import Foundation +import PurchaseConnector +import Flutter + +/// `PurchaseConnectorPlugin` is a `FlutterPlugin` implementation that acts as the bridge between Flutter and the PurchaseConnector iOS SDK. +/// This class is responsible for processing incoming method calls from the Dart layer (via a MethodChannel) and translating these calls to the appropriate tasks in the PurchaseConnector SDK. +@objc public class PurchaseConnectorPlugin : NSObject, FlutterPlugin { + + /// Methods channel name constant to be used across plugin. + private static let AF_PURCHASE_CONNECTOR_CHANNEL = "af-purchase-connector" + + /// Singleton instance of `PurchaseConnectorPlugin` ensures this plugin acts as a centralized point of contact for all method calls. + internal static let shared = PurchaseConnectorPlugin() + + /// An instance of `PurchaseConnector`. + /// This will be intentionally set to `nil` by default and will be initialized once we call the `configure` method via Flutter. + private var connector: PurchaseConnector? = nil + + /// Instance of method channel providing a bridge to Dart code. + private var methodChannel: FlutterMethodChannel? = nil + + private var logOptions: AutoLogPurchaseRevenueOptions = [] + + /// Constants used in method channel for Flutter calls. + private let logSubscriptionsKey = "logSubscriptions" + private let logInAppsKey = "logInApps" + private let sandboxKey = "sandbox" + + /// Private constructor, used to prevent direct instantiation of this class and ensure singleton behaviour. + private override init() {} + + /// Mandatory method needed to register the plugin with iOS part of Flutter app. + public static func register(with registrar: FlutterPluginRegistrar) { + /// Create a new method channel with the registrar. + shared.methodChannel = FlutterMethodChannel(name: AF_PURCHASE_CONNECTOR_CHANNEL, binaryMessenger: registrar.messenger()) + shared.methodChannel!.setMethodCallHandler(shared.methodCallHandler) + + } + + /// Method called when a Flutter method call occurs. It handles and routes flutter method invocations. + private func methodCallHandler(call: FlutterMethodCall, result: @escaping FlutterResult) { + switch(call.method) { + /// Match incoming flutter calls from Dart side to its corresponding native method. + case "configure": + configure(call: call, result: result) + case "startObservingTransactions": + startObservingTransactions(result: result) + case "stopObservingTransactions": + stopObservingTransactions(result: result) + default: + /// This condition handles an error scenario where the method call doesn't match any predefined cases. + result(FlutterMethodNotImplemented) + } + } + + /// This method corresponds to the 'configure' call from Flutter and initiates the PurchaseConnector instance. + private func configure(call: FlutterMethodCall, result: @escaping FlutterResult) { + /// Perform a guard check to ensure that we do not reconfigure an existing connector. + guard connector == nil else { + result(FlutterError(code: "401", message: "Connector already configured", details: nil)) + return + } + + /// Obtain a shared instance of PurchaseConnector + connector = PurchaseConnector.shared() + + /// Extract all the required parameters from Flutter arguments + let arguments = call.arguments as? [String: Any] + let logSubscriptions = arguments?[logSubscriptionsKey] as? Bool ?? false + let logInApps = arguments?[logInAppsKey] as? Bool ?? false + let sandbox = arguments?[sandboxKey] as? Bool ?? false + + /// Define an options variable to manage enabled options. + var options: AutoLogPurchaseRevenueOptions = [] + + /// Based on the arguments, insert the corresponding options. + if logSubscriptions { + options.insert(.autoRenewableSubscriptions) + } + if logInApps { + options.insert(.inAppPurchases) + } + + /// Update the PurchaseConnector instance with these options. + connector!.autoLogPurchaseRevenue = options + logOptions = options + connector!.isSandbox = sandbox + connector!.purchaseRevenueDelegate = self + + /// Report a successful operation back to Dart. + result(nil) + } + + /// This function starts the process of observing transactions in the iOS App Store. + private func startObservingTransactions(result: @escaping FlutterResult) { + connectorOperation(result: result) { connector in + // From the docs: If you called stopObservingTransactions API, set the autoLogPurchaseRevenue value before you call startObservingTransactions next time. + connector.autoLogPurchaseRevenue = self.logOptions + connector.startObservingTransactions() + + } + } + + /// This function stops the process of observing transactions in the iOS App Store. + private func stopObservingTransactions(result: @escaping FlutterResult) { + connectorOperation(result: result) { connector in + connector.stopObservingTransactions() + } + } + + /// Helper function used to extract common logic for operations on the connector. + private func connectorOperation(result: @escaping FlutterResult, operation: @escaping ((PurchaseConnector) -> Void)) { + guard connector != nil else { + result(FlutterError(code: "404", message: "Connector not configured, did you called `configure` first?", details: nil)) + return + } + /// Perform the operation with the current connector + operation(connector!) + + result(nil) + } +} + +/// Extension enabling `PurchaseConnectorPlugin` to conform to `PurchaseRevenueDelegate` +extension PurchaseConnectorPlugin: PurchaseRevenueDelegate { + /// Implementation of the `didReceivePurchaseRevenueValidationInfo` delegate method. + /// When the validation info comes back after a purchase, it is reported back to the Flutter via the method channel. + public func didReceivePurchaseRevenueValidationInfo(_ validationInfo: [AnyHashable : Any]?, error: Error?) { + var resMap: [AnyHashable : Any?] = [ + "validationInfo": validationInfo, + "error" : error?.asDictionary + ] + DispatchQueue.main.async { + self.methodChannel?.invokeMethod("didReceivePurchaseRevenueValidationInfo", arguments: resMap.toJSONString()) + } + } +} + +/// Extending `Error` to have a dictionary representation function. `asDictionary` will turn the current error instance into a dictionary containing `localizedDescription`, `domain` and `code` properties. +extension Error { + var asDictionary: [String: Any] { + var errorMap: [String: Any] = ["localizedDescription": self.localizedDescription] + if let nsError = self as? NSError { + errorMap["domain"] = nsError.domain + errorMap["code"] = nsError.code + } + return errorMap + } +} + +extension Dictionary { + + var jsonData: Data? { + return try? JSONSerialization.data(withJSONObject: self, options: [.prettyPrinted]) + } + + func toJSONString() -> String? { + if let jsonData = jsonData { + let jsonString = String(data: jsonData, encoding: .utf8) + return jsonString + } + return nil + } +} diff --git a/ios/appsflyer_sdk.podspec b/ios/appsflyer_sdk.podspec index 6a2631e5..00912f5d 100644 --- a/ios/appsflyer_sdk.podspec +++ b/ios/appsflyer_sdk.podspec @@ -1,25 +1,35 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# Pod::Spec.new do |s| s.name = 'appsflyer_sdk' - s.version = '6.16.2' + s.version = '6.17.1' s.summary = 'AppsFlyer Integration for Flutter' - s.description = <<-DESC -AppsFlyer is the market leader in mobile advertising attribution & analytics, helping marketers to pinpoint their targeting, optimize their ad spend and boost their ROI. - DESC + s.description = 'AppsFlyer is the market leader in mobile advertising attribution & analytics, helping marketers to pinpoint their targeting, optimize their ad spend and boost their ROI.' s.homepage = 'https://github.com/AppsFlyerSDK/flutter_appsflyer_sdk' s.license = { :type => 'MIT', :file => '../LICENSE' } s.author = { "Appsflyer" => "build@appsflyer.com" } s.source = { :git => "https://github.com/AppsFlyerSDK/flutter_appsflyer_sdk.git", :tag => s.version.to_s } - - + s.ios.deployment_target = '12.0' s.requires_arc = true s.static_framework = true + if defined?($AppsFlyerPurchaseConnector) + s.default_subspecs = 'Core', 'PurchaseConnector' + else + s.default_subspecs = 'Core' + end + + s.subspec 'Core' do |ss| + ss.source_files = 'Classes/**/*' + ss.public_header_files = 'Classes/**/*.h' + ss.dependency 'Flutter' + ss.ios.dependency 'AppsFlyerFramework','6.17.1' + end + + s.subspec 'PurchaseConnector' do |ss| + ss.dependency 'Flutter' + ss.ios.dependency 'PurchaseConnector', '6.17.1' + ss.source_files = 'PurchaseConnector/**/*' + ss.public_header_files = 'PurchaseConnector/**/*.h' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - s.ios.dependency 'AppsFlyerFramework','6.16.2' + ss.pod_target_xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) ENABLE_PURCHASE_CONNECTOR=1' } + end end diff --git a/lib/appsflyer_sdk.dart b/lib/appsflyer_sdk.dart index cee5ef4e..ff9df5ce 100644 --- a/lib/appsflyer_sdk.dart +++ b/lib/appsflyer_sdk.dart @@ -7,14 +7,28 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:json_annotation/json_annotation.dart'; + import 'src/callbacks.dart'; -part 'src/appsflyer_ad_revenue_data.dart'; part 'src/appsflyer_constants.dart'; -part 'src/appsflyer_consent.dart'; part 'src/appsflyer_invite_link_params.dart'; part 'src/appsflyer_options.dart'; -part 'src/appsflyer_request_listener.dart'; part 'src/appsflyer_sdk.dart'; part 'src/udl/deep_link_result.dart'; part 'src/udl/deeplink.dart'; +part 'src/purchase_connector/purchase_connector.dart'; +part 'src/purchase_connector/connector_callbacks.dart'; +part 'src/purchase_connector/missing_configuration_exception.dart'; +part 'src/purchase_connector/purchase_connector_configuration.dart'; +part 'src/purchase_connector/models/subscription_purchase.dart'; +part 'src/purchase_connector/models/in_app_purchase_validation_result.dart'; +part 'src/purchase_connector/models/product_purchase.dart'; +part 'src/purchase_connector/models/subscription_validation_result.dart'; +part 'src/purchase_connector/models/validation_failure_data.dart'; +part 'src/purchase_connector/models/jvm_throwable.dart'; +part 'src/purchase_connector/models/ios_error.dart'; +part 'src/appsflyer_consent.dart'; +part 'src/appsflyer_request_listener.dart'; +part 'appsflyer_sdk.g.dart'; +part 'src/appsflyer_ad_revenue_data.dart'; diff --git a/lib/appsflyer_sdk.g.dart b/lib/appsflyer_sdk.g.dart new file mode 100644 index 00000000..65275103 --- /dev/null +++ b/lib/appsflyer_sdk.g.dart @@ -0,0 +1,461 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'appsflyer_sdk.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SubscriptionPurchase _$SubscriptionPurchaseFromJson( + Map json) => + SubscriptionPurchase( + json['acknowledgementState'] as String, + json['canceledStateContext'] == null + ? null + : CanceledStateContext.fromJson( + json['canceledStateContext'] as Map), + json['externalAccountIdentifiers'] == null + ? null + : ExternalAccountIdentifiers.fromJson( + json['externalAccountIdentifiers'] as Map), + json['kind'] as String, + json['latestOrderId'] as String, + (json['lineItems'] as List) + .map((e) => + SubscriptionPurchaseLineItem.fromJson(e as Map)) + .toList(), + json['linkedPurchaseToken'] as String?, + json['pausedStateContext'] == null + ? null + : PausedStateContext.fromJson( + json['pausedStateContext'] as Map), + json['regionCode'] as String, + json['startTime'] as String, + json['subscribeWithGoogleInfo'] == null + ? null + : SubscribeWithGoogleInfo.fromJson( + json['subscribeWithGoogleInfo'] as Map), + json['subscriptionState'] as String, + json['testPurchase'] == null + ? null + : TestPurchase.fromJson(json['testPurchase'] as Map), + ); + +Map _$SubscriptionPurchaseToJson( + SubscriptionPurchase instance) => + { + 'acknowledgementState': instance.acknowledgementState, + 'canceledStateContext': instance.canceledStateContext, + 'externalAccountIdentifiers': instance.externalAccountIdentifiers, + 'kind': instance.kind, + 'latestOrderId': instance.latestOrderId, + 'lineItems': instance.lineItems, + 'linkedPurchaseToken': instance.linkedPurchaseToken, + 'pausedStateContext': instance.pausedStateContext, + 'regionCode': instance.regionCode, + 'startTime': instance.startTime, + 'subscribeWithGoogleInfo': instance.subscribeWithGoogleInfo, + 'subscriptionState': instance.subscriptionState, + 'testPurchase': instance.testPurchase, + }; + +CanceledStateContext _$CanceledStateContextFromJson( + Map json) => + CanceledStateContext( + json['developerInitiatedCancellation'] == null + ? null + : DeveloperInitiatedCancellation.fromJson( + json['developerInitiatedCancellation'] as Map), + json['replacementCancellation'] == null + ? null + : ReplacementCancellation.fromJson( + json['replacementCancellation'] as Map), + json['systemInitiatedCancellation'] == null + ? null + : SystemInitiatedCancellation.fromJson( + json['systemInitiatedCancellation'] as Map), + json['userInitiatedCancellation'] == null + ? null + : UserInitiatedCancellation.fromJson( + json['userInitiatedCancellation'] as Map), + ); + +Map _$CanceledStateContextToJson( + CanceledStateContext instance) => + { + 'developerInitiatedCancellation': instance.developerInitiatedCancellation, + 'replacementCancellation': instance.replacementCancellation, + 'systemInitiatedCancellation': instance.systemInitiatedCancellation, + 'userInitiatedCancellation': instance.userInitiatedCancellation, + }; + +DeveloperInitiatedCancellation _$DeveloperInitiatedCancellationFromJson( + Map json) => + DeveloperInitiatedCancellation(); + +Map _$DeveloperInitiatedCancellationToJson( + DeveloperInitiatedCancellation instance) => + {}; + +ReplacementCancellation _$ReplacementCancellationFromJson( + Map json) => + ReplacementCancellation(); + +Map _$ReplacementCancellationToJson( + ReplacementCancellation instance) => + {}; + +SystemInitiatedCancellation _$SystemInitiatedCancellationFromJson( + Map json) => + SystemInitiatedCancellation(); + +Map _$SystemInitiatedCancellationToJson( + SystemInitiatedCancellation instance) => + {}; + +UserInitiatedCancellation _$UserInitiatedCancellationFromJson( + Map json) => + UserInitiatedCancellation( + json['cancelSurveyResult'] == null + ? null + : CancelSurveyResult.fromJson( + json['cancelSurveyResult'] as Map), + json['cancelTime'] as String, + ); + +Map _$UserInitiatedCancellationToJson( + UserInitiatedCancellation instance) => + { + 'cancelSurveyResult': instance.cancelSurveyResult, + 'cancelTime': instance.cancelTime, + }; + +CancelSurveyResult _$CancelSurveyResultFromJson(Map json) => + CancelSurveyResult( + json['reason'] as String, + json['reasonUserInput'] as String, + ); + +Map _$CancelSurveyResultToJson(CancelSurveyResult instance) => + { + 'reason': instance.reason, + 'reasonUserInput': instance.reasonUserInput, + }; + +ExternalAccountIdentifiers _$ExternalAccountIdentifiersFromJson( + Map json) => + ExternalAccountIdentifiers( + json['externalAccountId'] as String, + json['obfuscatedExternalAccountId'] as String, + json['obfuscatedExternalProfileId'] as String, + ); + +Map _$ExternalAccountIdentifiersToJson( + ExternalAccountIdentifiers instance) => + { + 'externalAccountId': instance.externalAccountId, + 'obfuscatedExternalAccountId': instance.obfuscatedExternalAccountId, + 'obfuscatedExternalProfileId': instance.obfuscatedExternalProfileId, + }; + +SubscriptionPurchaseLineItem _$SubscriptionPurchaseLineItemFromJson( + Map json) => + SubscriptionPurchaseLineItem( + json['autoRenewingPlan'] == null + ? null + : AutoRenewingPlan.fromJson( + json['autoRenewingPlan'] as Map), + json['deferredItemReplacement'] == null + ? null + : DeferredItemReplacement.fromJson( + json['deferredItemReplacement'] as Map), + json['expiryTime'] as String, + json['offerDetails'] == null + ? null + : OfferDetails.fromJson(json['offerDetails'] as Map), + json['prepaidPlan'] == null + ? null + : PrepaidPlan.fromJson(json['prepaidPlan'] as Map), + json['productId'] as String, + ); + +Map _$SubscriptionPurchaseLineItemToJson( + SubscriptionPurchaseLineItem instance) => + { + 'autoRenewingPlan': instance.autoRenewingPlan, + 'deferredItemReplacement': instance.deferredItemReplacement, + 'expiryTime': instance.expiryTime, + 'offerDetails': instance.offerDetails, + 'prepaidPlan': instance.prepaidPlan, + 'productId': instance.productId, + }; + +OfferDetails _$OfferDetailsFromJson(Map json) => OfferDetails( + (json['offerTags'] as List?)?.map((e) => e as String).toList(), + json['basePlanId'] as String, + json['offerId'] as String?, + ); + +Map _$OfferDetailsToJson(OfferDetails instance) => + { + 'offerTags': instance.offerTags, + 'basePlanId': instance.basePlanId, + 'offerId': instance.offerId, + }; + +AutoRenewingPlan _$AutoRenewingPlanFromJson(Map json) => + AutoRenewingPlan( + json['autoRenewEnabled'] as bool?, + json['priceChangeDetails'] == null + ? null + : SubscriptionItemPriceChangeDetails.fromJson( + json['priceChangeDetails'] as Map), + ); + +Map _$AutoRenewingPlanToJson(AutoRenewingPlan instance) => + { + 'autoRenewEnabled': instance.autoRenewEnabled, + 'priceChangeDetails': instance.priceChangeDetails, + }; + +SubscriptionItemPriceChangeDetails _$SubscriptionItemPriceChangeDetailsFromJson( + Map json) => + SubscriptionItemPriceChangeDetails( + json['expectedNewPriceChargeTime'] as String, + json['newPrice'] == null + ? null + : Money.fromJson(json['newPrice'] as Map), + json['priceChangeMode'] as String, + json['priceChangeState'] as String, + ); + +Map _$SubscriptionItemPriceChangeDetailsToJson( + SubscriptionItemPriceChangeDetails instance) => + { + 'expectedNewPriceChargeTime': instance.expectedNewPriceChargeTime, + 'newPrice': instance.newPrice, + 'priceChangeMode': instance.priceChangeMode, + 'priceChangeState': instance.priceChangeState, + }; + +Money _$MoneyFromJson(Map json) => Money( + json['currencyCode'] as String, + (json['nanos'] as num).toInt(), + (json['units'] as num).toInt(), + ); + +Map _$MoneyToJson(Money instance) => { + 'currencyCode': instance.currencyCode, + 'nanos': instance.nanos, + 'units': instance.units, + }; + +DeferredItemReplacement _$DeferredItemReplacementFromJson( + Map json) => + DeferredItemReplacement( + json['productId'] as String, + ); + +Map _$DeferredItemReplacementToJson( + DeferredItemReplacement instance) => + { + 'productId': instance.productId, + }; + +PrepaidPlan _$PrepaidPlanFromJson(Map json) => PrepaidPlan( + json['allowExtendAfterTime'] as String?, + ); + +Map _$PrepaidPlanToJson(PrepaidPlan instance) => + { + 'allowExtendAfterTime': instance.allowExtendAfterTime, + }; + +PausedStateContext _$PausedStateContextFromJson(Map json) => + PausedStateContext( + json['autoResumeTime'] as String, + ); + +Map _$PausedStateContextToJson(PausedStateContext instance) => + { + 'autoResumeTime': instance.autoResumeTime, + }; + +SubscribeWithGoogleInfo _$SubscribeWithGoogleInfoFromJson( + Map json) => + SubscribeWithGoogleInfo( + json['emailAddress'] as String, + json['familyName'] as String, + json['givenName'] as String, + json['profileId'] as String, + json['profileName'] as String, + ); + +Map _$SubscribeWithGoogleInfoToJson( + SubscribeWithGoogleInfo instance) => + { + 'emailAddress': instance.emailAddress, + 'familyName': instance.familyName, + 'givenName': instance.givenName, + 'profileId': instance.profileId, + 'profileName': instance.profileName, + }; + +TestPurchase _$TestPurchaseFromJson(Map json) => + TestPurchase(); + +Map _$TestPurchaseToJson(TestPurchase instance) => + {}; + +InAppPurchaseValidationResult _$InAppPurchaseValidationResultFromJson( + Map json) => + InAppPurchaseValidationResult( + json['success'] as bool, + json['productPurchase'] == null + ? null + : ProductPurchase.fromJson( + json['productPurchase'] as Map), + json['failureData'] == null + ? null + : ValidationFailureData.fromJson( + json['failureData'] as Map), + ); + +Map _$InAppPurchaseValidationResultToJson( + InAppPurchaseValidationResult instance) => + { + 'success': instance.success, + 'productPurchase': instance.productPurchase, + 'failureData': instance.failureData, + }; + +InAppPurchaseValidationResultMap _$InAppPurchaseValidationResultMapFromJson( + Map json) => + InAppPurchaseValidationResultMap( + (json['result'] as Map).map( + (k, e) => MapEntry(k, + InAppPurchaseValidationResult.fromJson(e as Map)), + ), + ); + +Map _$InAppPurchaseValidationResultMapToJson( + InAppPurchaseValidationResultMap instance) => + { + 'result': instance.result, + }; + +ProductPurchase _$ProductPurchaseFromJson(Map json) => + ProductPurchase( + json['kind'] as String, + json['purchaseTimeMillis'] as String, + (json['purchaseState'] as num).toInt(), + (json['consumptionState'] as num).toInt(), + json['developerPayload'] as String, + json['orderId'] as String, + (json['purchaseType'] as num).toInt(), + (json['acknowledgementState'] as num).toInt(), + json['purchaseToken'] as String, + json['productId'] as String, + (json['quantity'] as num).toInt(), + json['obfuscatedExternalAccountId'] as String, + json['obfuscatedExternalProfileId'] as String, + json['regionCode'] as String, + ); + +Map _$ProductPurchaseToJson(ProductPurchase instance) => + { + 'kind': instance.kind, + 'purchaseTimeMillis': instance.purchaseTimeMillis, + 'purchaseState': instance.purchaseState, + 'consumptionState': instance.consumptionState, + 'developerPayload': instance.developerPayload, + 'orderId': instance.orderId, + 'purchaseType': instance.purchaseType, + 'acknowledgementState': instance.acknowledgementState, + 'purchaseToken': instance.purchaseToken, + 'productId': instance.productId, + 'quantity': instance.quantity, + 'obfuscatedExternalAccountId': instance.obfuscatedExternalAccountId, + 'obfuscatedExternalProfileId': instance.obfuscatedExternalProfileId, + 'regionCode': instance.regionCode, + }; + +SubscriptionValidationResult _$SubscriptionValidationResultFromJson( + Map json) => + SubscriptionValidationResult( + json['success'] as bool, + json['subscriptionPurchase'] == null + ? null + : SubscriptionPurchase.fromJson( + json['subscriptionPurchase'] as Map), + json['failureData'] == null + ? null + : ValidationFailureData.fromJson( + json['failureData'] as Map), + ); + +Map _$SubscriptionValidationResultToJson( + SubscriptionValidationResult instance) => + { + 'success': instance.success, + 'subscriptionPurchase': instance.subscriptionPurchase, + 'failureData': instance.failureData, + }; + +SubscriptionValidationResultMap _$SubscriptionValidationResultMapFromJson( + Map json) => + SubscriptionValidationResultMap( + (json['result'] as Map).map( + (k, e) => MapEntry(k, + SubscriptionValidationResult.fromJson(e as Map)), + ), + ); + +Map _$SubscriptionValidationResultMapToJson( + SubscriptionValidationResultMap instance) => + { + 'result': instance.result, + }; + +ValidationFailureData _$ValidationFailureDataFromJson( + Map json) => + ValidationFailureData( + (json['status'] as num).toInt(), + json['description'] as String, + ); + +Map _$ValidationFailureDataToJson( + ValidationFailureData instance) => + { + 'status': instance.status, + 'description': instance.description, + }; + +JVMThrowable _$JVMThrowableFromJson(Map json) => JVMThrowable( + json['type'] as String, + json['message'] as String, + json['stacktrace'] as String, + json['cause'] == null + ? null + : JVMThrowable.fromJson(json['cause'] as Map), + ); + +Map _$JVMThrowableToJson(JVMThrowable instance) => + { + 'type': instance.type, + 'message': instance.message, + 'stacktrace': instance.stacktrace, + 'cause': instance.cause, + }; + +IosError _$IosErrorFromJson(Map json) => IosError( + json['localizedDescription'] as String, + json['domain'] as String, + (json['code'] as num).toInt(), + ); + +Map _$IosErrorToJson(IosError instance) => { + 'localizedDescription': instance.localizedDescription, + 'domain': instance.domain, + 'code': instance.code, + }; diff --git a/lib/src/appsflyer_constants.dart b/lib/src/appsflyer_constants.dart index fe67ec2b..400298bb 100644 --- a/lib/src/appsflyer_constants.dart +++ b/lib/src/appsflyer_constants.dart @@ -3,7 +3,7 @@ part of appsflyer_sdk; enum EmailCryptType { EmailCryptTypeNone, EmailCryptTypeSHA256 } class AppsflyerConstants { - static const String PLUGIN_VERSION = "6.16.21"; + static const String PLUGIN_VERSION = "6.17.1"; static const String AF_DEV_KEY = "afDevKey"; static const String AF_APP_Id = "afAppId"; static const String AF_IS_DEBUG = "isDebug"; @@ -27,6 +27,36 @@ class AppsflyerConstants { static const String DISABLE_COLLECT_ASA = "disableCollectASA"; static const String DISABLE_ADVERTISING_IDENTIFIER = "disableAdvertisingIdentifier"; + + // Purchase Connector constants + static const String AF_PURCHASE_CONNECTOR_CHANNEL = "af-purchase-connector"; + static const String CONFIGURE_KEY = "configure"; + static const String LOG_SUBS_KEY = "logSubscriptionPurchase"; + static const String LOG_IN_APP_KEY = "logInAppPurchase"; + static const String SANDBOX_KEY = "sandbox"; + static const String VALIDATION_INFO = "validation_info"; + static const String ERROR = "error"; + static const String RESULT = "result"; + + // Purchase Connector listeners + static const String + SUBSCRIPTION_PURCHASE_VALIDATION_RESULT_LISTENER_ON_RESPONSE = + "SubscriptionPurchaseValidationResultListener#onResponse"; + static const String + SUBSCRIPTION_PURCHASE_VALIDATION_RESULT_LISTENER_ON_FAILURE = + "SubscriptionPurchaseValidationResultListener#onFailure"; + static const String IN_APP_VALIDATION_RESULT_LISTENER_ON_RESPONSE = + "InAppValidationResultListener#onResponse"; + static const String IN_APP_VALIDATION_RESULT_LISTENER_ON_FAILURE = + "InAppValidationResultListener#onFailure"; + static const String DID_RECEIVE_PURCHASE_REVENUE_VALIDATION_INFO = + "DidReceivePurchaseRevenueValidationInfo"; + + // Purchase Connector error messages + static const String MISSING_CONFIGURATION_EXCEPTION_MSG = + "Configuration is missing. Call PurchaseConnector.configure() first."; + static const String RE_CONFIGURE_ERROR_MSG = + "PurchaseConnector already configured."; } enum AFMediationNetwork { diff --git a/lib/src/purchase_connector/connector_callbacks.dart b/lib/src/purchase_connector/connector_callbacks.dart new file mode 100644 index 00000000..c908cfea --- /dev/null +++ b/lib/src/purchase_connector/connector_callbacks.dart @@ -0,0 +1,18 @@ +part of appsflyer_sdk; + +/// Type definition for a general purpose listener. +typedef PurchaseConnectorListener = Function(dynamic); +/// Type definition for a listener which gets called when the `PurchaseConnectorImpl` receives purchase revenue validation info for iOS. +typedef DidReceivePurchaseRevenueValidationInfo = Function( + Map? validationInfo, IosError? error); +/// Invoked when a 200 OK response is received from the server. +/// Note: An INVALID purchase is considered to be a successful response and will also be returned by this callback. +/// +/// [result] Server's response. +typedef OnResponse = Function(Map? result); + +/// Invoked when a network exception occurs or a non 200/OK response is received from the server. +/// +/// [result] The server's response. +/// [error] The exception that occurred during execution. +typedef OnFailure = Function(String result, JVMThrowable? error); diff --git a/lib/src/purchase_connector/missing_configuration_exception.dart b/lib/src/purchase_connector/missing_configuration_exception.dart new file mode 100644 index 00000000..eae23676 --- /dev/null +++ b/lib/src/purchase_connector/missing_configuration_exception.dart @@ -0,0 +1,11 @@ +part of appsflyer_sdk; +/// Exception thrown when the configuration is missing. +class MissingConfigurationException implements Exception { + final String message; + + MissingConfigurationException( + {this.message = AppsflyerConstants.MISSING_CONFIGURATION_EXCEPTION_MSG}); + + @override + String toString() => 'ConfigurationException: $message'; +} diff --git a/lib/src/purchase_connector/models/in_app_purchase_validation_result.dart b/lib/src/purchase_connector/models/in_app_purchase_validation_result.dart new file mode 100644 index 00000000..0846a180 --- /dev/null +++ b/lib/src/purchase_connector/models/in_app_purchase_validation_result.dart @@ -0,0 +1,33 @@ +part of appsflyer_sdk; + +@JsonSerializable() +class InAppPurchaseValidationResult { + + bool success; + ProductPurchase? productPurchase; + ValidationFailureData? failureData; + + InAppPurchaseValidationResult( + this.success, + this.productPurchase, + this.failureData + ); + + + + factory InAppPurchaseValidationResult.fromJson(Map json) => _$InAppPurchaseValidationResultFromJson(json); + + Map toJson() => _$InAppPurchaseValidationResultToJson(this); + +} + +@JsonSerializable() +class InAppPurchaseValidationResultMap{ + Map result; + + InAppPurchaseValidationResultMap(this.result); + factory InAppPurchaseValidationResultMap.fromJson(Map json) => _$InAppPurchaseValidationResultMapFromJson(json); + + Map toJson() => _$InAppPurchaseValidationResultMapToJson(this); + +} diff --git a/lib/src/purchase_connector/models/ios_error.dart b/lib/src/purchase_connector/models/ios_error.dart new file mode 100644 index 00000000..aa671066 --- /dev/null +++ b/lib/src/purchase_connector/models/ios_error.dart @@ -0,0 +1,15 @@ +part of appsflyer_sdk; + +@JsonSerializable() +class IosError{ + String localizedDescription; + String domain; + int code; + + + IosError(this.localizedDescription, this.domain, this.code); + + factory IosError.fromJson(Map json) => _$IosErrorFromJson(json); + + Map toJson() => _$IosErrorToJson(this); +} \ No newline at end of file diff --git a/lib/src/purchase_connector/models/jvm_throwable.dart b/lib/src/purchase_connector/models/jvm_throwable.dart new file mode 100644 index 00000000..04fb07e5 --- /dev/null +++ b/lib/src/purchase_connector/models/jvm_throwable.dart @@ -0,0 +1,16 @@ +part of appsflyer_sdk; + +@JsonSerializable() +class JVMThrowable{ + String type; + String message; + String stacktrace; + JVMThrowable? cause; + + JVMThrowable(this.type, this.message, this.stacktrace, this.cause); + + factory JVMThrowable.fromJson(Map json) => _$JVMThrowableFromJson(json); + + Map toJson() => _$JVMThrowableToJson(this); + +} \ No newline at end of file diff --git a/lib/src/purchase_connector/models/product_purchase.dart b/lib/src/purchase_connector/models/product_purchase.dart new file mode 100644 index 00000000..e6e3ceb6 --- /dev/null +++ b/lib/src/purchase_connector/models/product_purchase.dart @@ -0,0 +1,44 @@ +part of appsflyer_sdk; + +@JsonSerializable() +class ProductPurchase { + + String kind; + String purchaseTimeMillis; + int purchaseState; + int consumptionState; + String developerPayload; + String orderId; + int purchaseType; + int acknowledgementState; + String purchaseToken; + String productId; + int quantity; + String obfuscatedExternalAccountId; + String obfuscatedExternalProfileId; + String regionCode; + + ProductPurchase( + this.kind, + this.purchaseTimeMillis, + this.purchaseState, + this.consumptionState, + this.developerPayload, + this.orderId, + this.purchaseType, + this.acknowledgementState, + this.purchaseToken, + this.productId, + this.quantity, + this.obfuscatedExternalAccountId, + this.obfuscatedExternalProfileId, + this.regionCode + ); + + + + factory ProductPurchase.fromJson(Map json) => _$ProductPurchaseFromJson(json); + + Map toJson() => _$ProductPurchaseToJson(this); + +} \ No newline at end of file diff --git a/lib/src/purchase_connector/models/subscription_purchase.dart b/lib/src/purchase_connector/models/subscription_purchase.dart new file mode 100644 index 00000000..55ede1e6 --- /dev/null +++ b/lib/src/purchase_connector/models/subscription_purchase.dart @@ -0,0 +1,343 @@ +part of appsflyer_sdk; + +@JsonSerializable() +class SubscriptionPurchase { + + String acknowledgementState; + CanceledStateContext? canceledStateContext; + ExternalAccountIdentifiers? externalAccountIdentifiers; + String kind; + String latestOrderId; + List lineItems; + String? linkedPurchaseToken; + PausedStateContext? pausedStateContext; + String regionCode; + String startTime; + SubscribeWithGoogleInfo? subscribeWithGoogleInfo; + String subscriptionState; + TestPurchase? testPurchase; + + SubscriptionPurchase( + this.acknowledgementState, + this.canceledStateContext, + this.externalAccountIdentifiers, + this.kind, + this.latestOrderId, + this.lineItems, + this.linkedPurchaseToken, + this.pausedStateContext, + this.regionCode, + this.startTime, + this.subscribeWithGoogleInfo, + this.subscriptionState, + this.testPurchase + ); + + + + factory SubscriptionPurchase.fromJson(Map json) => _$SubscriptionPurchaseFromJson(json); + + Map toJson() => _$SubscriptionPurchaseToJson(this); + +} + + +@JsonSerializable() +class CanceledStateContext { + + DeveloperInitiatedCancellation? developerInitiatedCancellation; + ReplacementCancellation? replacementCancellation; + SystemInitiatedCancellation? systemInitiatedCancellation; + UserInitiatedCancellation? userInitiatedCancellation; + + CanceledStateContext( + this.developerInitiatedCancellation, + this.replacementCancellation, + this.systemInitiatedCancellation, + this.userInitiatedCancellation + ); + + + + factory CanceledStateContext.fromJson(Map json) => _$CanceledStateContextFromJson(json); + + Map toJson() => _$CanceledStateContextToJson(this); + +} + +@JsonSerializable() +class DeveloperInitiatedCancellation{ + DeveloperInitiatedCancellation(); + factory DeveloperInitiatedCancellation.fromJson(Map json) => _$DeveloperInitiatedCancellationFromJson(json); + + Map toJson() => _$DeveloperInitiatedCancellationToJson(this); +} + +@JsonSerializable() +class ReplacementCancellation{ + ReplacementCancellation(); + factory ReplacementCancellation.fromJson(Map json) => _$ReplacementCancellationFromJson(json); + + Map toJson() => _$ReplacementCancellationToJson(this); +} + +@JsonSerializable() +class SystemInitiatedCancellation{ + SystemInitiatedCancellation(); + factory SystemInitiatedCancellation.fromJson(Map json) => _$SystemInitiatedCancellationFromJson(json); + + Map toJson() => _$SystemInitiatedCancellationToJson(this); +} + + +@JsonSerializable() +class UserInitiatedCancellation { + + CancelSurveyResult? cancelSurveyResult; + String cancelTime; + + UserInitiatedCancellation( + this.cancelSurveyResult, + this.cancelTime + ); + + + + factory UserInitiatedCancellation.fromJson(Map json) => _$UserInitiatedCancellationFromJson(json); + + Map toJson() => _$UserInitiatedCancellationToJson(this); + +} + +@JsonSerializable() +class CancelSurveyResult { + + String reason; + String reasonUserInput; + + CancelSurveyResult( + this.reason, + this.reasonUserInput + ); + + + + factory CancelSurveyResult.fromJson(Map json) => _$CancelSurveyResultFromJson(json); + + Map toJson() => _$CancelSurveyResultToJson(this); + +} + +@JsonSerializable() +class ExternalAccountIdentifiers { + + String externalAccountId; + String obfuscatedExternalAccountId; + String obfuscatedExternalProfileId; + + ExternalAccountIdentifiers( + this.externalAccountId, + this.obfuscatedExternalAccountId, + this.obfuscatedExternalProfileId + ); + + + + factory ExternalAccountIdentifiers.fromJson(Map json) => _$ExternalAccountIdentifiersFromJson(json); + + Map toJson() => _$ExternalAccountIdentifiersToJson(this); + +} + +@JsonSerializable() +class SubscriptionPurchaseLineItem { + + AutoRenewingPlan? autoRenewingPlan; + DeferredItemReplacement? deferredItemReplacement; + String expiryTime; + OfferDetails? offerDetails; + PrepaidPlan? prepaidPlan; + String productId; + + SubscriptionPurchaseLineItem( + this.autoRenewingPlan, + this.deferredItemReplacement, + this.expiryTime, + this.offerDetails, + this.prepaidPlan, + this.productId + ); + + + + factory SubscriptionPurchaseLineItem.fromJson(Map json) => _$SubscriptionPurchaseLineItemFromJson(json); + + Map toJson() => _$SubscriptionPurchaseLineItemToJson(this); + +} + +@JsonSerializable() +class OfferDetails { + + List? offerTags; + String basePlanId; + String? offerId; + + OfferDetails( + this.offerTags, + this.basePlanId, + this.offerId + ); + + + + factory OfferDetails.fromJson(Map json) => _$OfferDetailsFromJson(json); + + Map toJson() => _$OfferDetailsToJson(this); + +} + +@JsonSerializable() +class AutoRenewingPlan { + + bool? autoRenewEnabled; + SubscriptionItemPriceChangeDetails? priceChangeDetails; + + AutoRenewingPlan( + this.autoRenewEnabled, + this.priceChangeDetails + ); + + + + factory AutoRenewingPlan.fromJson(Map json) => _$AutoRenewingPlanFromJson(json); + + Map toJson() => _$AutoRenewingPlanToJson(this); + +} + +@JsonSerializable() +class SubscriptionItemPriceChangeDetails { + + String expectedNewPriceChargeTime; + Money? newPrice; + String priceChangeMode; + String priceChangeState; + + SubscriptionItemPriceChangeDetails( + this.expectedNewPriceChargeTime, + this.newPrice, + this.priceChangeMode, + this.priceChangeState + ); + + + + factory SubscriptionItemPriceChangeDetails.fromJson(Map json) => _$SubscriptionItemPriceChangeDetailsFromJson(json); + + Map toJson() => _$SubscriptionItemPriceChangeDetailsToJson(this); + +} + +@JsonSerializable() +class Money { + + String currencyCode; + int nanos; + int units; + + Money( + this.currencyCode, + this.nanos, + this.units + ); + + + + factory Money.fromJson(Map json) => _$MoneyFromJson(json); + + Map toJson() => _$MoneyToJson(this); + +} +@JsonSerializable() +class DeferredItemReplacement { + + String productId; + + DeferredItemReplacement( + this.productId + ); + + + + factory DeferredItemReplacement.fromJson(Map json) => _$DeferredItemReplacementFromJson(json); + + Map toJson() => _$DeferredItemReplacementToJson(this); + +} + +@JsonSerializable() +class PrepaidPlan { + + String? allowExtendAfterTime; + + PrepaidPlan( + this.allowExtendAfterTime + ); + + + + factory PrepaidPlan.fromJson(Map json) => _$PrepaidPlanFromJson(json); + + Map toJson() => _$PrepaidPlanToJson(this); + +} + +@JsonSerializable() +class PausedStateContext { + + String autoResumeTime; + + PausedStateContext( + this.autoResumeTime + ); + + + + factory PausedStateContext.fromJson(Map json) => _$PausedStateContextFromJson(json); + + Map toJson() => _$PausedStateContextToJson(this); + +} +@JsonSerializable() +class SubscribeWithGoogleInfo { + + String emailAddress; + String familyName; + String givenName; + String profileId; + String profileName; + + SubscribeWithGoogleInfo( + this.emailAddress, + this.familyName, + this.givenName, + this.profileId, + this.profileName + ); + + + + factory SubscribeWithGoogleInfo.fromJson(Map json) => _$SubscribeWithGoogleInfoFromJson(json); + + Map toJson() => _$SubscribeWithGoogleInfoToJson(this); + +} + +@JsonSerializable() +class TestPurchase{ + TestPurchase(); + factory TestPurchase.fromJson(Map json) => _$TestPurchaseFromJson(json); + + Map toJson() => _$TestPurchaseToJson(this); +} \ No newline at end of file diff --git a/lib/src/purchase_connector/models/subscription_validation_result.dart b/lib/src/purchase_connector/models/subscription_validation_result.dart new file mode 100644 index 00000000..b9294176 --- /dev/null +++ b/lib/src/purchase_connector/models/subscription_validation_result.dart @@ -0,0 +1,32 @@ +part of appsflyer_sdk; + +@JsonSerializable() +class SubscriptionValidationResult { + + bool success; + SubscriptionPurchase? subscriptionPurchase; + ValidationFailureData? failureData; + + SubscriptionValidationResult( + this.success, + this.subscriptionPurchase, + this.failureData + ); + + + + factory SubscriptionValidationResult.fromJson(Map json) => _$SubscriptionValidationResultFromJson(json); + + Map toJson() => _$SubscriptionValidationResultToJson(this); + +} + +@JsonSerializable() +class SubscriptionValidationResultMap{ + Map result; + + SubscriptionValidationResultMap(this.result); + factory SubscriptionValidationResultMap.fromJson(Map json) => _$SubscriptionValidationResultMapFromJson(json); + + Map toJson() => _$SubscriptionValidationResultMapToJson(this); +} diff --git a/lib/src/purchase_connector/models/validation_failure_data.dart b/lib/src/purchase_connector/models/validation_failure_data.dart new file mode 100644 index 00000000..57b3d137 --- /dev/null +++ b/lib/src/purchase_connector/models/validation_failure_data.dart @@ -0,0 +1,20 @@ +part of appsflyer_sdk; + +@JsonSerializable() +class ValidationFailureData { + + int status; + String description; + + ValidationFailureData( + this.status, + this.description + ); + + + + factory ValidationFailureData.fromJson(Map json) => _$ValidationFailureDataFromJson(json); + + Map toJson() => _$ValidationFailureDataToJson(this); + +} \ No newline at end of file diff --git a/lib/src/purchase_connector/purchase_connector.dart b/lib/src/purchase_connector/purchase_connector.dart new file mode 100644 index 00000000..e962823e --- /dev/null +++ b/lib/src/purchase_connector/purchase_connector.dart @@ -0,0 +1,254 @@ +part of appsflyer_sdk; + +/// Interface representing a purchase connector. +abstract class PurchaseConnector { + /// Starts observing transactions. + void startObservingTransactions(); + + /// Stops observing transactions. + void stopObservingTransactions(); + + /// Sets the listener for Android subscription validation results. + /// + /// [onResponse] Function to be executed when a successful response is received. + /// [onFailure] Function to be executed when a failure occurs (network exception or non 200/OK response from the server). + void setSubscriptionValidationResultListener( + OnResponse? onResponse, + OnFailure? onFailure); + + /// Sets the listener for Android in-app validation results. + /// + /// [onResponse] Function to be executed when a successful response is received. + /// [onFailure] Function to be executed when a failure occurs (network exception or non 200/OK response from the server). + + void setInAppValidationResultListener( + OnResponse? onResponse, + OnFailure? onFailure); + + /// Sets the listener for iOS subscription and in-app validation results. + /// Parameter: + /// [callback] the function to be executed when `DidReceivePurchaseRevenueValidationInfo` is called. + void setDidReceivePurchaseRevenueValidationInfo( + DidReceivePurchaseRevenueValidationInfo? callback); + + /// Creates a new PurchaseConnector instance. + /// Parameter: + /// [config] the configuration to be used when creating a new `PurchaseConnector` instance. + factory PurchaseConnector({PurchaseConnectorConfiguration? config}) => + _PurchaseConnectorImpl(config: config); +} + +/// The implementation of the PurchaseConnector. +/// +/// This class is responsible for establishing a connection with Appsflyer purchase connector, +/// starting/stopping observing transactions, setting listeners for various validation results. +class _PurchaseConnectorImpl implements PurchaseConnector { + /// Singleton instance of the PurchaseConnectorImpl. + static _PurchaseConnectorImpl? _instance; + + /// Method channel to communicate with the Appsflyer Purchase Connector. + final MethodChannel _methodChannel; + + /// Response handler for SubscriptionValidationResult (Android). + OnResponse? _arsOnResponse; + + /// Failure handler for SubscriptionValidationResult (Android). + OnFailure? _arsOnFailure; + + /// Response handler for InAppPurchaseValidationResult (Android). + OnResponse? _viapOnResponse; + + /// Failure handler for InAppPurchaseValidationResult (Android). + OnFailure? _viapOnFailure; + + /// Callback handler for receiving validation info for iOS. + DidReceivePurchaseRevenueValidationInfo? + _didReceivePurchaseRevenueValidationInfo; + + /// Internal constructor. Initializes the instance and sets up method call handler. + _PurchaseConnectorImpl._internal( + this._methodChannel, PurchaseConnectorConfiguration config) { + _methodChannel.setMethodCallHandler(_methodCallHandler); + _methodChannel.invokeMethod(AppsflyerConstants.CONFIGURE_KEY, { + AppsflyerConstants.LOG_SUBS_KEY: config.logSubscriptions, + AppsflyerConstants.LOG_IN_APP_KEY: config.logInApps, + AppsflyerConstants.SANDBOX_KEY: config.sandbox, + }); + } + + /// Factory constructor. + /// + /// This factory ensures that only a single instance of `PurchaseConnectorImpl` is used throughout the program + /// by implementing the Singleton design pattern. If an instance already exists, it's returned. + /// + /// The [config] parameter is optional and is used only when creating the first instance of `PurchaseConnectorImpl`. + /// Once an instance is created, the same instance will be returned in subsequent calls, and the [config] + /// parameter will be ignored. Thus, it's valid to call this factory without a config if an instance already exists. + /// + /// If there is no existing instance and the [config] is not provided, a `MissingConfigurationException` will be thrown. + factory _PurchaseConnectorImpl({PurchaseConnectorConfiguration? config}) { + if (_instance == null && config == null) { + // no instance exist and config not provided. We Can't create instance without config + throw MissingConfigurationException(); + } else if (_instance == null && config != null) { + // no existing instance. Create new instance and apply config + MethodChannel methodChannel = + const MethodChannel(AppsflyerConstants.AF_PURCHASE_CONNECTOR_CHANNEL); + _instance = _PurchaseConnectorImpl._internal(methodChannel, config); + } else if (_instance != null && config != null) { + debugPrint(AppsflyerConstants.RE_CONFIGURE_ERROR_MSG); + } + return _instance!; + } + + /// Starts observing the transactions. + @override + void startObservingTransactions() { + _methodChannel.invokeMethod("startObservingTransactions"); + } + + /// Stops observing the transactions. + @override + void stopObservingTransactions() { + _methodChannel.invokeMethod("stopObservingTransactions"); + } + + /// Sets the function to be executed when iOS validation info is received. + @override + void setDidReceivePurchaseRevenueValidationInfo( + DidReceivePurchaseRevenueValidationInfo? callback) { + _didReceivePurchaseRevenueValidationInfo = callback; + } + + /// Sets the listener for Android in-app validation results. + /// + /// [onResponse] Function to be executed when a successful response is received. + /// [onFailure] Function to be executed when a failure occurs (network exception or non 200/OK response from the server). + @override + void setInAppValidationResultListener( + OnResponse? onResponse, + OnFailure? onFailure) { + _viapOnResponse = onResponse; + _viapOnFailure = onFailure; + } + + /// Sets the listener for Android subscription validation results. + /// + /// [onResponse] Function to be executed when a successful response is received. + /// [onFailure] Function to be executed when a failure occurs (network exception or non 200/OK response from the server). + @override + void setSubscriptionValidationResultListener( + OnResponse? onResponse, + OnFailure? onFailure) { + _arsOnResponse = onResponse; + _arsOnFailure = onFailure; + } + + /// Method call handler for different operations. Called by the _methodChannel. + Future _methodCallHandler(MethodCall call) async { + dynamic callMap = jsonDecode(call.arguments); + switch (call.method) { + case AppsflyerConstants + .SUBSCRIPTION_PURCHASE_VALIDATION_RESULT_LISTENER_ON_RESPONSE: + _handleSubscriptionPurchaseValidationResultListenerOnResponse(callMap); + break; + case AppsflyerConstants + .SUBSCRIPTION_PURCHASE_VALIDATION_RESULT_LISTENER_ON_FAILURE: + _handleSubscriptionPurchaseValidationResultListenerOnFailure(callMap); + break; + case AppsflyerConstants.IN_APP_VALIDATION_RESULT_LISTENER_ON_RESPONSE: + _handleInAppValidationResultListenerOnResponse(callMap); + break; + case AppsflyerConstants.IN_APP_VALIDATION_RESULT_LISTENER_ON_FAILURE: + _handleInAppValidationResultListenerOnFailure(callMap); + break; + case AppsflyerConstants.DID_RECEIVE_PURCHASE_REVENUE_VALIDATION_INFO: + _handleDidReceivePurchaseRevenueValidationInfo(callMap); + break; + default: + throw ArgumentError("Method not found."); + } + } + + /// Handles response for the subscription purchase validation result listener. + /// + /// [callbackData] is the callback data expected in the form of a map. + void _handleSubscriptionPurchaseValidationResultListenerOnResponse( + dynamic callbackData) { + _handleValidationResultListenerOnResponse( + {"result": callbackData}, + _arsOnResponse, + (value) => SubscriptionValidationResultMap.fromJson(value).result, + ); + } + + /// Handles response for the in-app validation result listener. + /// + /// [callbackData] is the callback data expected in the form of a map. + void _handleInAppValidationResultListenerOnResponse( + dynamic callbackData) { + _handleValidationResultListenerOnResponse( + {"result": callbackData}, + _viapOnResponse, + (value) => InAppPurchaseValidationResultMap.fromJson(value).result, + ); + } + + /// Handles failure for the subscription purchase validation result listener. + /// + /// [callbackData] is the callback data expected in the form of a map. + void _handleSubscriptionPurchaseValidationResultListenerOnFailure( + Map callbackData) { + _handleValidationResultListenerOnFailure(callbackData, _arsOnFailure); + } + + /// Handles failure for the in-app validation result listener. + /// + /// [callbackData] is the callback data expected in the form of a map. + void _handleInAppValidationResultListenerOnFailure(dynamic callbackData) { + _handleValidationResultListenerOnFailure(callbackData, _viapOnFailure); + } + + /// Handles the reception of purchase revenue validation info. + /// + /// [callbackData] is the callback data expected in the form of a map. + void _handleDidReceivePurchaseRevenueValidationInfo(dynamic callbackData) { + var validationInfo = callbackData[AppsflyerConstants.VALIDATION_INFO] + as Map?; + var errorMap = + callbackData[AppsflyerConstants.ERROR] as Map?; + var error = errorMap != null ? IosError.fromJson(errorMap) : null; + if (_didReceivePurchaseRevenueValidationInfo != null) { + _didReceivePurchaseRevenueValidationInfo!(validationInfo, error); + } + } + + /// Handles the response for a validation result listener. + /// + /// [callbackData] is the callback data expected in the form of a map. + /// [onResponse] is a function to be called upon response. + /// [converter] is a function used for converting `[callbackData]` to result type `T` + void _handleValidationResultListenerOnResponse(dynamic callbackData, + OnResponse? onResponse, Map? Function(dynamic) converter) { + Map? res = converter(callbackData); + if (onResponse != null) { + onResponse(res); + } else { + } + } + + /// Handles failure for a validation result listener. + /// + /// [callbackData] is the callback data expected in the form of a map. + /// [onFailureCallback] is a function to be called on failure. + void _handleValidationResultListenerOnFailure( + dynamic callbackData, OnFailure? onFailureCallback) { + var resultMsg = callbackData[AppsflyerConstants.RESULT] as String; + var errorMap = + callbackData[AppsflyerConstants.ERROR] as Map?; + var error = errorMap != null ? JVMThrowable.fromJson(errorMap) : null; + if (onFailureCallback != null) { + onFailureCallback(resultMsg, error); + } + } +} diff --git a/lib/src/purchase_connector/purchase_connector_configuration.dart b/lib/src/purchase_connector/purchase_connector_configuration.dart new file mode 100644 index 00000000..3832dc7d --- /dev/null +++ b/lib/src/purchase_connector/purchase_connector_configuration.dart @@ -0,0 +1,16 @@ +part of appsflyer_sdk; + +/// Contains the configuration settings for a `PurchaseConnector`. +/// +/// This class controls automatic logging of In-App purchase and subscription events. +/// It also allows setting a sandbox environment for validation. +class PurchaseConnectorConfiguration { + bool logSubscriptions; + bool logInApps; + bool sandbox; + + PurchaseConnectorConfiguration( + {this.logSubscriptions = false, + this.logInApps = false, + this.sandbox = false}); +} diff --git a/pubspec.yaml b/pubspec.yaml index 2c67b59d..1aaa948c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: appsflyer_sdk description: A Flutter plugin for AppsFlyer SDK. Supports iOS and Android. -version: 6.16.21 +version: 6.17.1 homepage: https://github.com/AppsFlyerSDK/flutter_appsflyer_sdk @@ -11,6 +11,7 @@ environment: dependencies: flutter: sdk: flutter + json_annotation: ^4.9.0 dev_dependencies: flutter_test: @@ -18,6 +19,8 @@ dev_dependencies: test: ^1.16.5 mockito: ^5.4.4 effective_dart: ^1.3.0 + build_runner: ^2.3.0 + json_serializable: ^6.5.4 flutter: From 85c3360a43f850279eb46e088d8b15bb4a526ebe Mon Sep 17 00:00:00 2001 From: Dani Koza <103039399+Dani-Koza-AF@users.noreply.github.com> Date: Wed, 30 Jul 2025 10:03:27 +0300 Subject: [PATCH 27/42] Immediate actions to prevent NullPointerExceptions (#403) 1. Fixed startSDKwithHandler() method: Null checks moved inside lambda execution 2. Fixed runOnUIThread() method: Added null check for mCallbackChannel. 3. Immediately return initSdk method when dev key is missing. --- .../appsflyersdk/AppsflyerSdkPlugin.java | 74 ++++++++++--------- .../appsflyer/appsflyersdk/LogMessages.java | 1 + 2 files changed, 41 insertions(+), 34 deletions(-) diff --git a/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java b/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java index a5245f7a..6e65f686 100644 --- a/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java +++ b/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java @@ -390,27 +390,27 @@ private void startSDKwithHandler(MethodCall call, final Result result) { appsFlyerLib.start(activity, null, new AppsFlyerRequestListener() { @Override public void onSuccess() { - if (mMethodChannel != null) { - uiThreadHandler.post(() -> mMethodChannel.invokeMethod("onSuccess", null)); - } else { - Log.e(AF_PLUGIN_TAG, LogMessages.METHOD_CHANNEL_IS_NULL); - result.error("NULL_OBJECT", LogMessages.METHOD_CHANNEL_IS_NULL, null); - } + uiThreadHandler.post(() -> { + if (mMethodChannel != null) { + mMethodChannel.invokeMethod("onSuccess", null); + } else { + Log.e(AF_PLUGIN_TAG, LogMessages.METHOD_CHANNEL_IS_NULL + " - SDK started successfully but callback `onSuccess` failed"); + } + }); } @Override public void onError(final int errorCode, final String errorMessage) { - if (mMethodChannel != null) { - uiThreadHandler.post(() -> { + uiThreadHandler.post(() -> { + if (mMethodChannel != null) { HashMap errorDetails = new HashMap<>(); errorDetails.put("errorCode", errorCode); errorDetails.put("errorMessage", errorMessage); mMethodChannel.invokeMethod("onError", errorDetails); - }); - } else { - Log.e(AF_PLUGIN_TAG, LogMessages.METHOD_CHANNEL_IS_NULL); - result.error("NULL_OBJECT", LogMessages.METHOD_CHANNEL_IS_NULL, null); - } + } else { + Log.e(AF_PLUGIN_TAG, LogMessages.METHOD_CHANNEL_IS_NULL + " - SDK failed to start: " + errorMessage); + } + }); } }); result.success(null); @@ -734,29 +734,33 @@ private void runOnUIThread(final Object data, final String callbackName, final S new Runnable() { @Override public void run() { - Log.d("Callbacks", "Calling invokeMethod with: " + data); - JSONObject args = new JSONObject(); - try { - args.put("id", callbackName); - //return data for UDL - if (callbackName.equals(AppsFlyerConstants.AF_UDL_CALLBACK)) { - DeepLinkResult dp = (DeepLinkResult) data; - args.put("deepLinkStatus", dp.getStatus().toString()); - if (dp.getError() != null) { - args.put("deepLinkError", dp.getError().toString()); - } - if (dp.getStatus() == DeepLinkResult.Status.FOUND) { - args.put("deepLinkObj", dp.getDeepLink().getClickEvent()); + if (mCallbackChannel != null) { + Log.d(AF_PLUGIN_TAG, "Calling invokeMethod with: " + data); + JSONObject args = new JSONObject(); + try { + args.put("id", callbackName); + //return data for UDL + if (callbackName.equals(AppsFlyerConstants.AF_UDL_CALLBACK)) { + DeepLinkResult dp = (DeepLinkResult) data; + args.put("deepLinkStatus", dp.getStatus().toString()); + if (dp.getError() != null) { + args.put("deepLinkError", dp.getError().toString()); + } + if (dp.getStatus() == DeepLinkResult.Status.FOUND) { + args.put("deepLinkObj", dp.getDeepLink().getClickEvent()); + } + } else { // return data for conversionData and OAOA + JSONObject dataJSON = (JSONObject) data; + args.put("status", status); + args.put("data", data.toString()); } - } else { // return data for conversionData and OAOA - JSONObject dataJSON = (JSONObject) data; - args.put("status", status); - args.put("data", data.toString()); + } catch (JSONException e) { + e.printStackTrace(); } - } catch (JSONException e) { - e.printStackTrace(); + mCallbackChannel.invokeMethod("callListener", args.toString()); + } else { + Log.e(AF_PLUGIN_TAG, "CallbackChannel is null, cannot invoke method: " + callbackName); } - mCallbackChannel.invokeMethod("callListener", args.toString()); } } ); @@ -941,7 +945,9 @@ private void initSdk(MethodCall call, final MethodChannel.Result result) { String afDevKey = (String) call.argument(AppsFlyerConstants.AF_DEV_KEY); if (afDevKey == null || afDevKey.equals("")) { - result.error(null, "AF Dev Key is empty", "AF dev key cannot be empty"); + Log.e(AF_PLUGIN_TAG, LogMessages.AF_DEV_KEY_IS_EMPTY); + result.error("INIT_ERROR", LogMessages.AF_DEV_KEY_IS_EMPTY, null); + return; } boolean advertiserIdDisabled = (boolean) call.argument(AppsFlyerConstants.DISABLE_ADVERTISING_IDENTIFIER); diff --git a/android/src/main/java/com/appsflyer/appsflyersdk/LogMessages.java b/android/src/main/java/com/appsflyer/appsflyersdk/LogMessages.java index 8adf9747..2bf5b5e4 100644 --- a/android/src/main/java/com/appsflyer/appsflyersdk/LogMessages.java +++ b/android/src/main/java/com/appsflyer/appsflyersdk/LogMessages.java @@ -10,4 +10,5 @@ private LogMessages() { public static final String METHOD_CHANNEL_IS_NULL = "mMethodChannel is null, cannot invoke the callback"; public static final String ACTIVITY_NOT_ATTACHED_TO_ENGINE = "Activity isn't attached to the flutter engine"; public static final String ERROR_WHILE_SETTING_CONSENT = "Error while setting consent data: "; + public static final String AF_DEV_KEY_IS_EMPTY = "AppsFlyer dev key is empty"; } From cbf7c678a8502b3a1eee1070bf7fdbaebb1cd0eb Mon Sep 17 00:00:00 2001 From: Dani Koza <103039399+Dani-Koza-AF@users.noreply.github.com> Date: Sun, 3 Aug 2025 11:24:50 +0300 Subject: [PATCH 28/42] Latest release updates - Docs + new API for Android (#406) * add disableAppSetId() method for AppSet ID opt-out * added a simple test for the new api * docs * another doc update * documents fix lint * doc lint * doc * doc fix * last time doc fix * docs --- CHANGELOG.md | 133 ++++++++++++++++-- README.md | 36 +++-- .../appsflyersdk/AppsflyerSdkPlugin.java | 7 + doc/API.md | 14 ++ doc/AdvancedAPI.md | 3 + doc/DMA.md | 29 +++- lib/src/appsflyer_sdk.dart | 7 + test/appsflyer_sdk_test.dart | 7 + 8 files changed, 200 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6ccaebf..4e80d3e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,21 @@ # Versions + +## 6.17.1 + +- Android: Bug fix for users who expirienced `NullPointerExceptions`. +- Android: added a new [disableAppSetId()](https://dev.appsflyer.com/hc/docs/android-sdk-reference-appsflyerlib#disableappsetid) method for AppSet ID opting-out. +- iOS: Added support for Google Integrated Conversion ([ICM](https://support.google.com/google-ads/answer/16203286)) measurement. +- Documentation small fixes. +- Purchase Connector is rolled out to production. + + ## 6.16.21 + - Bug fix for users who reported Locale issue on Android, fixed Locale issue by forcing toUpperCase(Locale.ENGLISH) - Expanded the unit–tests + ## 6.16.2 + - setConsentData is now deprecated! - setConsentDataV2 is the new and recommended way to set manual user consent. - Added getVersionNumber, returns the plugin's version. @@ -12,230 +25,320 @@ - Closed a few potential memory leaks. - Update iOS version to 6.16.2 - Update Android version to 6.16.2 + ## 6.15.2 + - Fixed NullPointerException issue on Android that some clients had. - Fixed Android MediationNetwork enum issue. - Update iOS version to 6.15.3 - Update Android version to 6.15.2 + ## 6.15.1 + - Implementation of the new logAdRevenue API for iOS and Android - Documentation update for the new logAdRevenue API - Update iOS version to 6.15.1 - Update Android version to 6.15.1 + ## 6.14.3 + - Fixed mapOptions issue with manualStart - Inherit Privacy Manifest from the native iOS SDK via Cocoapods - Bump iOS version to 6.14.3 + ## 6.14.2 + - Bump version to iOS v6.14.2 and Android v6.14.0 -- Added Privacy Manifest to support Apple latest changes: https://developer.apple.com/documentation/bundleresources/privacy_manifest_files +- Added Privacy Manifest to support Apple latest changes: + ## 6.13.2+1 + - Hotfix for manualStart on iOS + ## 6.13.2 + - Added new APIs such as `anonymizeUser` , `performOnDeepLinking` - Added to the `startSDK` API, `onSuccess` and `onError` callbacks - Update to iOS SDK to v6.13.2 + ## 6.13.0+2 + - Update to iOS SDK to v6.13.1 + ## 6.13.0+1 + - Added enableTCFDataCollection , setConsentData with AppsFlyerConsent class - Added new boolean option to AppsFlyerOption class , manualStart - Added startSDK API - Updated readme and elaborated on the new APIs + ## 6.12.2 + - Update to Android SDK to v6.12.2 & iOS SDK to v6.12.2 - Deprecated CreateOneLinkHttpTask updated to LinkGenerator - Fixed Gradle 8.0 issue - Documented API and removed unused imports + ## 6.11.3 + - null pointer exception fix for android, push notification bug fix & ios sdk 6.11.2 + ## 6.11.2 + - update to Android SDK to v6.11.2 + ## 6.11.1 + - update to Android SDK to v6.11.1 + ## 6.10.1 + - update to Android SDK to v6.10.3 & iOS SDK to v6.10.1 + ## 6.9.3 + - update to Android SDK to v6.9.3 & iOS SDK to v6.9.1 - Added `addPushNotificationDeepLinkPath` API - Added `setCustomerIdAndLogSession` API for android + ## 6.8.2 + - update to android v6.8.2 + ## 6.8.0 + - The API `enableLocationCollection` has been removed. - The API `setDisableNetworkData` has been added. - The AD_ID permission has been added to the plugin. - Updated AppsFlyer Android SDK to v6.8.0 - Updated AppsFlyer iOS SDK to v6.8.0 + ## 6.5.2+2 + ## 6.5.2+1 + - New APIs: getOutOfStore, setOutOfStore, setResolveDeepLinkURLs, setPartnerData + ## 6.5.2 + - Updated AppsFlyer Android SDK to v6.5.2 - Updated AppsFlyer iOS SDK to v6.5.2 + ## 6.4.4+2 + ## 6.4.0+2 + ## 6.4.0+1 -- Added nullable in deeplink object + +- Added nullable in deeplink object - Remove of local stream import + ## 6.4.0 + - Updated to 6.4.0 in iOS & Android SDK - Dedicated class for UDL for handling deeplink -- New API `setSharingFilterForPartners`.`setSharingFilter` & `setSharingFilterForAllPartners` APIs were deprecated. +- New API `setSharingFilterForPartners`.`setSharingFilter` & `setSharingFilterForAllPartners` APIs were deprecated. - setIntent is not required anymore in MainActivity (Android) - application(_:open:sourceApplication:annotation:) is not required anymore in AppDelegate (iOS) - application(_:open:options:) is not required anymore in AppDelegate (iOS) - application(_:continue:restorationHandler:) is not required anymore in AppDelegate (iOS) ## 6.3.5+3 + rollback to previous version + ## 6.3.5+2 + Removed streams from the plugin + ## 6.3.5+1 + Added setCurrentDeviceLanguage API + ## 6.3.5 + - Updated AppsFlyer iOS SDK to v6.3.5 ## 6.3.3+1 + - fix JNI issue ## 6.3.3-nullsafety.0 + - change to local broadcast ## 6.3.2-nullsafety.0 + - Update to SDK v6.3.2 and added support for disabling advertiser ID on Android ## 6.3.0-nullsafety.1 + - Added effective dart package for linter rules ## 6.3.0-nullsafety.0 + - Update iOS & Android to SDK v6.3.0 ## 6.2.6-nullsafety.1 + - Fix for deeplinking in iOS ## 6.2.6-nullsafety.0 + - Update for iOS SDK V6.2.6 - Refactoring for SKAD network feature ## 6.2.4-nullsafety.5 + - Added support for strict mode (kids app) - Added support for wait for att status API ## 6.2.4+4-nullsafety + - Fix small bug with validateAndLogInAppIosPurchase API ## 6.2.4+3-nullsafety + - Small fix for enableFacebookDeferredApplinks, useReceiptValidationSandbox, disableSKAdNetwork, setPushNotification APIs in iOS ## 6.2.4+2-nullsafety + - Added disable SKAD API ## 6.2.4+1-nullsafety + - Fix for SKAD ## 6.2.4 + - Update to iOS SDK v6.2.4 ## 6.2.3+2 + - Flutter 2.0 update including null safety support ## 6.2.3+2-beta + - Flutter 2.0 update including null safety support ## 6.2.3+1 + - Added enableFacebookDeferredApplinks API ## 6.2.3 + - Update to iOS SDK V6.2.3 ## 6.2.1+7 + - Refactor for user invite feature ## 6.2.1+6 + - Added callbacks support for purchase validation API ## 6.2.1+5 + - Added support for useReceiptValidationSandbox API ## 6.2.1+4 + - Seperated purchase validation API to iOS/Android ## 6.2.1+3 + - Fixed Unified deeplink crush on first launch ## 6.2.1+2 + - Hot Fix ## 6.2.1+1 + - Added support for push notification API ## 6.2.1 + - Update iOS to v6.2.1 - Added support for Unified Deeplink - Fixed deeplinks issues both for Android & iOS ## 6.2.0+2 + - Revert back to version 6.2.0 ## 6.2.0+1 + - Added Unified Deeplinking for Android ## 6.2.0 + - Update both iOS & Android to v6.2.0 ## 6.0.5+3 + - Fixed `FormatException` caused by iOS side ## 6.0.5+2 + - Switch to callbacks for `onAppOpenAttribution` and `onConversionData` ## 6.0.5+1 + - Fixed `updateServerUninstallToken` on iOS ## 6.0.5 + - Update SDK version to: - - Android: 5.4.5 - - iOS: 6.0.5 + - Android: 5.4.5 + - iOS: 6.0.5 - Update Google install referrer to 2.1 -- Added support for: https://support.appsflyer.com/hc/en-us/articles/207032066#additional-apis-kids-apps +- Added support for: - Fixed typo in `validateAndLogInAppPurchase` -## 6.0.3+5 +## 6.0.3+5 + - Add null check for context in Android -## 6.0.3+4 -- Fixed bug with sending arguments with methodChannel +## 6.0.3+4 + +- Fixed bug with sending arguments with methodChannel ## 6.0.3+3 -- Added the functions: -`logCrossPromotionAndOpenStore` -`logCrossPromotionImpression` -`setAppInviteOneLinkID` + +- Added the functions: +`logCrossPromotionAndOpenStore` +`logCrossPromotionImpression` +`setAppInviteOneLinkID` `generateInviteLink` ## 6.0.3+2 + - Removed AppTrackingTransparency framework ## 6.0.3+1 + - Updated AppsFlyer iOS SDK to v6.0.3 ## 6.0.2+1 + - Fixed the issue in the example app on Android platform - Updated AppsFlyer SDK to v5.4.3 ## 6.0.2 + - iOS sdk version is now 6.0.2 and support AppTrackingTransparency framework - Android sdk version is 5.4.1 ## 5.4.1+1 + - Added documentation - Added secured links to README ## 5.4.1 - Updated AppsFlyer SDK to v5.4.1 -- Added `sharedFilter` support +- Added `sharedFilter` support ## 5.2.0+3 @@ -277,7 +380,7 @@ Added setCurrentDeviceLanguage API ## 1.1.3 - Added getAppsFlyerUID function to get a device unique user id - + ## 1.1.2 - Updated appsflyer framework to 4.9.0 diff --git a/README.md b/README.md index ba5a3314..9fe93add 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,35 @@ - +# appsflyer-flutter-plugin -# appsflyer-flutter-plugin +![AppsFlyer Logo](https://massets.appsflyer.com/wp-content/uploads/2018/06/20092440/static-ziv_1TP.png) -[![pub package](https://img.shields.io/pub/v/appsflyer_sdk.svg)](https://pub.dartlang.org/packages/appsflyer_sdk) +[![pub package](https://img.shields.io/pub/v/appsflyer_sdk.svg)](https://pub.dartlang.org/packages/appsflyer_sdk) ![Coverage](https://raw.githubusercontent.com/AppsFlyerSDK/appsflyer-flutter-plugin/master/coverage_badge.svg) -🛠 In order for us to provide optimal support, we would kindly ask you to submit any issues to support@appsflyer.com +🛠 In order for us to provide optimal support, we would kindly ask you to submit any issues to > *When submitting an issue please specify your AppsFlyer sign-up (account) email , your app ID , production steps, logs, code snippets and any additional relevant information.* +## SDK Versions -### This plugin is built for +- Android AppsFlyer SDK **v6.17.0** +- iOS AppsFlyer SDK **v6.17.1** -- Android AppsFlyer SDK **v6.16.2** -- iOS AppsFlyer SDK **v6.16.2** +### Purchase Connector versions -## ❗❗ Breaking changes when updating to v6.x.x❗❗ +- Android 2.1.1 +- iOS 6.17.1 + +## ❗❗ Breaking changes when updating to v6.x.x❗❗ If you have used one of the removed/changed APIs, please check the integration guide for the updated instructions. -- From version `6.11.2`, the `setPushNotification` will not work in iOS. [Please use our new API `sendPushNotificationData` when receiving a notification on flutter side](/doc/API.md#sendPushNotificationData). +- From version `6.11.2`, the `setPushNotification` will not work in iOS. [Please use our new API `sendPushNotificationData` when receiving a notification on flutter side](/doc/API.md#sendPushNotificationData). - From version `6.8.0`, the `enableLocationCollection` has been removed from the plugin. -- From version `6.4.0`, UDL (Unified deep link) now as a dedicated class with getters for handling the deeplink result. +- From version `6.4.0`, UDL (Unified deep link) now as a dedicated class with getters for handling the deeplink result. [Check the full UDL guide](https://github.com/AppsFlyerSDK/appsflyer-flutter-plugin/blob/master/doc/Guides.md#-3-unified-deep-linking). -`setSharingFilter` & `setSharingFilterForAllPartners` APIs are deprecated. +`setSharingFilter` & `setSharingFilterForAllPartners` APIs are deprecated. Instead use the [new API `setSharingFilterForPartners`](https://github.com/AppsFlyerSDK/appsflyer-flutter-plugin/blob/RD-69098/update6.4.0%26more/doc/API.md#setSharingFilterForPartners). - From version `6.3.5+2`, Remove stream from the plugin (no change is needed if you use callbacks for handling deeplink). @@ -48,20 +52,24 @@ Instead use the [new API `setSharingFilterForPartners`](https://github.com/AppsF | validateAndLogInAppPurchase | validateAndLogInAppIosPurchase/validateAndLogInAppAndroidPurchase | ### Important notice + - Switch `ConversionData` and `OnAppOpenAttribution` to be based on callbacks instead of streams from plugin version `6.0.5+2`. ## AD_ID permission for Android -In v6.8.0 of the AppsFlyer SDK, we added the normal permission `com.google.android.gms.permission.AD_ID` to the SDK's AndroidManifest, + +In v6.8.0 of the AppsFlyer SDK, we added the normal permission `com.google.android.gms.permission.AD_ID` to the SDK's AndroidManifest, to allow the SDK to collect the Android Advertising ID on apps targeting API 33. If your app is targeting children, you need to revoke this permission to comply with Google's Data policy. -You can read more about it [here](https://dev.appsflyer.com/hc/docs/install-android-sdk#the-ad_id-permission). +You can read more about it in the [Android SDK installation guide](https://dev.appsflyer.com/hc/docs/install-android-sdk#the-ad_id-permission). + +## 📖 Guides -## 📖 Guides - [Adding the SDK to your project](/doc/Installation.md) - [Initializing the SDK](/doc/BasicIntegration.md) - [In-app Events](/doc/InAppEvents.md) - [Deep Linking](/doc/DeepLink.md) - [Advanced APIs](/doc/AdvancedAPI.md) - [Testing the integration](/doc/Testing.md) +- [Purchase Connector](/doc/PurchaseConnector.md) <- **New addition** - [APIs](/doc/API.md) - [Sample App](/example) diff --git a/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java b/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java index 6e65f686..74b607db 100644 --- a/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java +++ b/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java @@ -355,6 +355,9 @@ public void onMethodCall(MethodCall call, Result result) { case "logAdRevenue": logAdRevenue(call, result); break; + case "disableAppSetId": + disableAppSetId(call, result); + break; default: result.notImplemented(); break; @@ -1108,6 +1111,10 @@ private Map replaceNullValues(Map map) { return newMap; } + private void disableAppSetId(MethodCall call, Result result) { + AppsFlyerLib.getInstance().disableAppSetId(); + result.success(null); + } @Override public void onAttachedToEngine(FlutterPluginBinding binding) { diff --git a/doc/API.md b/doc/API.md index 9861fe06..d05a80f9 100644 --- a/doc/API.md +++ b/doc/API.md @@ -53,6 +53,7 @@ - [setOutOfStore](#setOutOfStore) - [getOutOfStore](#getOutOfStore) - [setDisableNetworkData](#setDisableNetworkData) +- [disableAppSetId](#disableAppSetId) - [performOnDeepLinking](#performondeeplinking) - [logAdRevenue](#logAdRevenue) - Since 6.15.1 @@ -861,6 +862,19 @@ _Example:_ } ``` --- +** `void disableAppSetId()`** + +**Android Only!** + +Disables AppSet ID collection. Starting with v6.17.0, the SDK can automatically collect the AppSet ID. Use this method to opt-out of AppSet ID collection for privacy compliance. + +_Example:_ +```dart + if(Platform.isAndroid){ + appsflyerSdk.disableAppSetId(); + } +``` +--- ** `void performOnDeepLinking()`** diff --git a/doc/AdvancedAPI.md b/doc/AdvancedAPI.md index 07857834..e41720b9 100644 --- a/doc/AdvancedAPI.md +++ b/doc/AdvancedAPI.md @@ -20,6 +20,7 @@ You can read more about iOS Uninstall Measurement in our [knowledge base](https: You can register the uninstall token with AppsFlyer by modifying your `AppDelegate.m` file, add the following function call with your uninstall token inside [didRegisterForRemoteNotificationsWithDeviceToken](https://developer.apple.com/reference/uikit/uiapplicationdelegate). **Example:** + ```objective-c @import AppsFlyerLib; @@ -59,6 +60,7 @@ A complete list of supported parameters is available [here](https://support.apps **`Future setAppInviteOneLinkID(String oneLinkID, Function callback)`** 2. Utilize the AppsFlyerInviteLinkParams class to set the query params in the user invite link: + ```dart class AppsFlyerInviteLinkParams { final String channel; @@ -75,6 +77,7 @@ class AppsFlyerInviteLinkParams { 3. Call the generateInviteLink API to generate the user invite link. Use the success and error callbacks for handling. **Full example:** + ```dart // Setting the OneLinkID appsFlyerSdk.setAppInviteOneLinkID('OnelinkID', diff --git a/doc/DMA.md b/doc/DMA.md index 92d44c32..b5c56e1e 100644 --- a/doc/DMA.md +++ b/doc/DMA.md @@ -1,14 +1,18 @@ # Set Consent For DMA Compliance -Following the DMA regulations that were set by the European Commission, Google (and potentially other SRNs in the future) require to send them the user's consent data in order to interact with them during the attribution process. In our latest plugin update (6.16.2), we've introduced two new public APIs, enhancing our support for user consent and data collection preferences in line with evolving digital market regulations. +Following the DMA regulations that were set by the European Commission, Google (and potentially other SRNs in the future) require to send them the user's consent data in order to interact with them during the attribution process. In our latest plugin update (6.16.2), we've introduced two new public APIs, enhancing our support for user consent and data collection preferences in line with evolving digital market regulations. There are two alternative ways for gathering consent data: - Through a Consent Management Platform (CMP): If the app uses a CMP that complies with the Transparency and Consent Framework (TCF) v2.2 protocol, the SDK can automatically retrieve the consent details. -### OR + +**OR** + - Through a dedicated SDK API: Developers can pass Google's required consent data directly to the SDK using a specific API designed for this purpose. ## Use CMP to collect consent data + A CMP compatible with TCF v2.2 collects DMA consent data and stores it in NSUserDefaults (iOS) and SharedPreferences (Android). To enable the SDK to access this data and include it with every event, follow these steps: + 1. Call `appsflyerSdk.enableTCFDataCollection(true)` 2. Initialize the SDK in manual start mode by setting `manualStart: true` in the `AppsFlyerOptions` when creating the AppsflyerSdk instance. 3. Use the CMP to decide if you need the consent dialog in the current session to acquire the consent data. If you need the consent dialog move to step 4, otherwise move to step 5. @@ -43,11 +47,16 @@ if (cmpManager.hasConsent()) { } ``` -## Manually collect consent data -### setConsentData is now **deprecated**. use setConsentDataV2 +## Manually collect consent data + + +### setConsentData is now **deprecated**. use [setConsentDataV2](#setconsentdatav2-recommended-api-for-manual-consent-collection---since-6162) + + If your app does not use a CMP compatible with TCF v2.2, use the SDK API detailed below to provide the consent data directly to the SDK, distinguishing between cases when GDPR applies or not. ### When GDPR applies to the user + If GDPR applies to the user, perform the following: 1. Given that GDPR is applicable to the user, determine whether the consent data is already stored for this session. @@ -93,6 +102,7 @@ appsflyerSdk.initSdk( ### When GDPR does not apply to the user If GDPR doesn't apply to the user perform the following: + 1. Create an AppsFlyerConsent object using `nonGDPRUser` method that doesn't accept any parameters. 2. Call `appsflyerSdk.setConsentData(consentData)` with the AppsFlyerConsent object. 3. Initialize the SDK using `appsflyerSdk.initSdk()`. @@ -122,6 +132,7 @@ appsflyerSdk.initSdk( ``` ## setConsentDataV2 (Recommended API for Manual Consent Collection) - since 6.16.2 + 🚀 **Why Use setConsentDataV2?**
The setConsentDataV2 API is the new and improved way to manually provide user consent data to the AppsFlyer SDK. @@ -134,6 +145,7 @@ It replaces the now deprecated setConsentData method, offering several improveme If your app previously used setConsentData, it is highly recommended to migrate to setConsentDataV2 for a more flexible and robust solution. 📌 **API Reference** + ```dart void setConsentDataV2({ bool? isUserSubjectToGDPR, @@ -141,9 +153,10 @@ void setConsentDataV2({ bool? consentForAdsPersonalization, bool? hasConsentForAdStorage }) -``` +``` + +### Parameters -**Parameters** | Parameter | Type | Description | | -------- | -------- | -------- | | isUserSubjectToGDPR | bool? | Indicates if the user is subject to GDPR regulations. | @@ -155,6 +168,7 @@ void setConsentDataV2({ - These values should be collected from the user via an appropriate **UI or consent prompt** before calling this method. 📌 **Example Usage** + ```dart // Initialize AppsFlyerOptions with manualStart: true final AppsFlyerOptions options = AppsFlyerOptions( @@ -184,7 +198,8 @@ appsflyerSdk.initSdk( // Start the SDK appsflyerSdk.startSDK(); -``` +``` + 📌 **Notes**
â€ĸ You should call this method **before initializing the AppsFlyer SDK** if possible, or at least before `startSDK()` when using manual initialization.
â€ĸ Ensure you collect consent **legally and transparently** from the user before passing these values. diff --git a/lib/src/appsflyer_sdk.dart b/lib/src/appsflyer_sdk.dart index 45cf4bae..a7a6a793 100644 --- a/lib/src/appsflyer_sdk.dart +++ b/lib/src/appsflyer_sdk.dart @@ -613,6 +613,13 @@ class AppsflyerSdk { _methodChannel.invokeMethod("setDisableNetworkData", disable); } + /// Disables AppSet ID collection (Android only). + /// Starting with v6.17.0, the SDK can automatically collect the AppSet ID. + /// Use this method to opt-out of AppSet ID collection. + void disableAppSetId() { + _methodChannel.invokeMethod("disableAppSetId"); + } + /// Retrieves the current plugin version. String getVersionNumber() { return AppsflyerConstants.PLUGIN_VERSION; diff --git a/test/appsflyer_sdk_test.dart b/test/appsflyer_sdk_test.dart index 4997fe60..bcd396e6 100644 --- a/test/appsflyer_sdk_test.dart +++ b/test/appsflyer_sdk_test.dart @@ -56,6 +56,7 @@ void main() { case 'setConsentData': case 'enableTCFDataCollection': case 'setDisableNetworkData': + case 'disableAppSetId': case 'setPartnerData': case 'setResolveDeepLinkURLs': case 'setPushNotification': @@ -373,5 +374,11 @@ void main() { expect(selectedMethod, 'setDisableAdvertisingIdentifiers'); expect(capturedArguments, true); }); + + test('check disableAppSetId call', () async { + instance.disableAppSetId(); + + expect(selectedMethod, 'disableAppSetId'); + }); }); } From 12f7a35d75149147a4bc17a47371251b822d8c14 Mon Sep 17 00:00:00 2001 From: Dani Koza <103039399+Dani-Koza-AF@users.noreply.github.com> Date: Sun, 24 Aug 2025 16:41:36 +0300 Subject: [PATCH 29/42] Updated to AppsFlyer SDK v6.17.3 for both Android and iOS (#410) * iOS >> Added support for Setting StoreKit2 properly * versions bump * Dart + Android implementation * iOS implementation * docs update * purchase connector doc update for StoreKitV2 support --- CHANGELOG.md | 8 + README.md | 6 +- android/build.gradle | 2 +- .../appsflyersdk/AppsFlyerConstants.java | 2 +- .../appsflyersdk/AppsflyerSdkPlugin.java | 107 ++++++++++ doc/API.md | 112 ++++++++++ doc/AdvancedAPI.md | 53 ++++- doc/PurchaseConnector.md | 132 +++++++++++- .../xcshareddata/xcschemes/Runner.xcscheme | 18 ++ example/lib/home_container.dart | 201 +++++++++++++----- example/lib/main_page.dart | 31 +++ ios/Classes/AppsflyerSdkPlugin.h | 2 +- ios/Classes/AppsflyerSdkPlugin.m | 86 ++++++++ .../PurchaseConnectorPlugin.swift | 17 +- ios/appsflyer_sdk.podspec | 6 +- lib/appsflyer_sdk.dart | 2 + lib/src/af_purchase_details.dart | 41 ++++ lib/src/appsflyer_constants.dart | 4 +- lib/src/appsflyer_sdk.dart | 20 ++ .../purchase_connector.dart | 1 + .../purchase_connector_configuration.dart | 17 +- .../purchase_connector/store_kit_version.dart | 51 +++++ pubspec.yaml | 2 +- 23 files changed, 848 insertions(+), 73 deletions(-) create mode 100644 lib/src/af_purchase_details.dart create mode 100644 lib/src/purchase_connector/store_kit_version.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 3033507f..6f50a708 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Versions +## 6.17.3 + +- Updated to AppsFlyer SDK v6.17.3 for both Android and iOS +- Added validateAndLogInAppPurchaseV2 API (Beta) for improved cross-platform purchase validation +- Unified AFPurchaseDetails data structure for type-safe purchase validation +- Enhanced error handling and consistent API across Android and iOS +- Maintains backward compatibility with existing purchase validation methods + ## 6.17.1 - Android: Bug fix for users who expirienced `NullPointerExceptions`. diff --git a/README.md b/README.md index 9fe93add..83add27e 100644 --- a/README.md +++ b/README.md @@ -11,13 +11,13 @@ ## SDK Versions -- Android AppsFlyer SDK **v6.17.0** -- iOS AppsFlyer SDK **v6.17.1** +- Android AppsFlyer SDK **v6.17.3** +- iOS AppsFlyer SDK **v6.17.3** ### Purchase Connector versions - Android 2.1.1 -- iOS 6.17.1 +- iOS 6.17.3 ## ❗❗ Breaking changes when updating to v6.x.x❗❗ diff --git a/android/build.gradle b/android/build.gradle index fb0b2b21..a5d3aa66 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -53,7 +53,7 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.appcompat:appcompat:1.0.0' - implementation 'com.appsflyer:af-android-sdk:6.17.0' + implementation 'com.appsflyer:af-android-sdk:6.17.3' implementation 'com.android.installreferrer:installreferrer:2.2' // implementation 'androidx.core:core-ktx:1.13.1' if (includeConnector) { diff --git a/android/src/main/java/com/appsflyer/appsflyersdk/AppsFlyerConstants.java b/android/src/main/java/com/appsflyer/appsflyersdk/AppsFlyerConstants.java index 88444166..408e86c7 100644 --- a/android/src/main/java/com/appsflyer/appsflyersdk/AppsFlyerConstants.java +++ b/android/src/main/java/com/appsflyer/appsflyersdk/AppsFlyerConstants.java @@ -1,7 +1,7 @@ package com.appsflyer.appsflyersdk; public final class AppsFlyerConstants { - final static String PLUGIN_VERSION = "6.17.1"; + final static String PLUGIN_VERSION = "6.17.3"; final static String AF_APP_INVITE_ONE_LINK = "appInviteOneLink"; final static String AF_HOST_PREFIX = "hostPrefix"; final static String AF_HOST_NAME = "hostName"; diff --git a/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java b/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java index 74b607db..27c19675 100644 --- a/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java +++ b/android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.java @@ -11,9 +11,12 @@ import com.appsflyer.AFAdRevenueData; import com.appsflyer.AFLogger; +import com.appsflyer.AFPurchaseDetails; +import com.appsflyer.AFPurchaseType; import com.appsflyer.AppsFlyerConsent; import com.appsflyer.AppsFlyerConversionListener; import com.appsflyer.AppsFlyerInAppPurchaseValidatorListener; +import com.appsflyer.AppsFlyerInAppPurchaseValidationCallback; import com.appsflyer.AppsFlyerLib; import com.appsflyer.AppsFlyerProperties; import com.appsflyer.MediationNetwork; @@ -286,6 +289,9 @@ public void onMethodCall(MethodCall call, Result result) { case "validateAndLogInAppAndroidPurchase": validateAndLogInAppPurchase(call, result); break; + case "validateAndLogInAppPurchaseV2": + validateAndLogInAppPurchaseV2(call, result); + break; case "getAppsFlyerUID": getAppsFlyerUID(result); break; @@ -796,6 +802,107 @@ private void validateAndLogInAppPurchase(MethodCall call, Result result) { result.success(null); } + private void validateAndLogInAppPurchaseV2(MethodCall call, Result result) { + try { + // Get the complete purchase details map + Map purchaseDetailsMap = (Map) call.argument("purchaseDetails"); + Map additionalParameters = (Map) call.argument("additionalParameters"); + + if (purchaseDetailsMap == null) { + result.error("INVALID_ARGUMENTS", "Purchase details cannot be null", null); + return; + } + + if (additionalParameters == null) { + additionalParameters = new HashMap<>(); + } + + // Extract fields from purchase details map + String purchaseTypeString = (String) purchaseDetailsMap.get("purchaseType"); + String purchaseToken = (String) purchaseDetailsMap.get("purchaseToken"); + String productId = (String) purchaseDetailsMap.get("productId"); + + // Validate required fields + if (purchaseTypeString == null || purchaseToken == null || productId == null) { + result.error("INVALID_ARGUMENTS", "Purchase details must contain purchaseType, purchaseToken, and productId", null); + return; + } + + // Map Dart enum values to Android AFPurchaseType enum + AFPurchaseType purchaseType = mapPurchaseType(purchaseTypeString); + if (purchaseType == null) { + result.error("INVALID_PURCHASE_TYPE", "Invalid purchase type: " + purchaseTypeString + ". Expected: 'subscription' or 'one_time_purchase'", null); + return; + } + + // Create AFPurchaseDetails object + AFPurchaseDetails purchaseDetails = new AFPurchaseDetails( + purchaseType, + purchaseToken, + productId + ); + + Log.d(AF_PLUGIN_TAG, "validateAndLogInAppPurchaseV2 called with " + purchaseDetailsMap); + + AppsFlyerLib.getInstance().validateAndLogInAppPurchase( + purchaseDetails, + additionalParameters, + new AppsFlyerInAppPurchaseValidationCallback() { + @Override + public void onInAppPurchaseValidationFinished(@NonNull Map validationFinishedResult) { + Log.d(AF_PLUGIN_TAG, "Purchase validation V2 response arrived"); + + // Convert the result to a format Flutter can understand + Map flutterResult = new HashMap<>(); + for (Map.Entry entry : validationFinishedResult.entrySet()) { + flutterResult.put(entry.getKey(), entry.getValue()); + } + + result.success(flutterResult); + } + + @Override + public void onInAppPurchaseValidationError(@NonNull Map validationErrorResult) { + Log.d(AF_PLUGIN_TAG, "Purchase validation V2 returned error"); + + String errorMessage = "Purchase validation failed"; + if (validationErrorResult.containsKey("error_message")) { + errorMessage = (String) validationErrorResult.get("error_message"); + } + + // Convert error result to Flutter format + Map flutterErrorResult = new HashMap<>(); + for (Map.Entry entry : validationErrorResult.entrySet()) { + flutterErrorResult.put(entry.getKey(), entry.getValue()); + } + + result.error("VALIDATION_ERROR", errorMessage, flutterErrorResult); + } + } + ); + + } catch (Exception e) { + Log.e(AF_PLUGIN_TAG, "Error in validateAndLogInAppPurchaseV2: " + e.getMessage(), e); + result.error("VALIDATION_ERROR", "Purchase validation failed: " + e.getMessage(), null); + } + } + + /** + * Maps Dart enum string to Android AFPurchaseType enum. + * @param purchaseTypeString The string representation from Dart + * @return AFPurchaseType enum or null if invalid + */ + private AFPurchaseType mapPurchaseType(String purchaseTypeString) { + switch (purchaseTypeString) { + case "subscription": + return AFPurchaseType.SUBSCRIPTION; + case "one_time_purchase": + return AFPurchaseType.ONE_TIME_PURCHASE; + default: + return null; + } + } + private void registerValidatorListener() { AppsFlyerInAppPurchaseValidatorListener validatorListener = new AppsFlyerInAppPurchaseValidatorListener() { @Override diff --git a/doc/API.md b/doc/API.md index d05a80f9..a57323cd 100644 --- a/doc/API.md +++ b/doc/API.md @@ -6,6 +6,8 @@ - [AppsFlyerOptions](#appsflyer-options) - [AdRevenueData](#AdRevenueData) - [AFMediationNetwork](#AFMediationNetwork) +- [AFPurchaseDetails](#AFPurchaseDetails) +- [AFPurchaseType](#AFPurchaseType) ## Methods - [initSdk](#initSdk) @@ -35,6 +37,7 @@ - [getHostPrefix](#getHostPrefix) - [updateServerUninstallToken](#updateServerUninstallToken) - [Validate Purchase](#validatePurchase) +- [validateAndLogInAppPurchaseV2](#validatePurchaseV2) - [sendPushNotificationData](#sendPushNotificationData) - [addPushNotificationDeepLinkPath](#addPushNotificationDeepLinkPath) - [User Invite](#userInvite) @@ -511,6 +514,54 @@ appsFlyerSdk.updateServerUninstallToken("token"); --- ** Validate Purchase** +***Cross-Platform V2 API (Recommended - BETA):*** + +> âš ī¸ **BETA Feature**: This API is currently in beta. While it's stable and recommended for new implementations, please test thoroughly in your environment before production use. + +**`Future> validateAndLogInAppPurchaseV2(AFPurchaseDetails purchaseDetails, {Map? additionalParameters})`** + +The new unified purchase validation API that works across both Android and iOS platforms. This is the recommended approach for validating in-app purchases. + +| Parameter | Type | Description | +|-----------|------|-------------| +| `purchaseDetails` | `AFPurchaseDetails` | Purchase details containing type, token, and product ID | +| `additionalParameters` | `Map?` | Optional additional parameters | + +**AFPurchaseDetails:** +| Property | Type | Description | +|----------|------|-------------| +| `purchaseType` | `AFPurchaseType` | Type of purchase (oneTimePurchase or subscription) | +| `purchaseToken` | `String` | Purchase token from the app store | +| `productId` | `String` | Product identifier | + +**AFPurchaseType:** +- `AFPurchaseType.oneTimePurchase` - For one-time in-app purchases +- `AFPurchaseType.subscription` - For subscription purchases + +_Example:_ +```dart +// Create purchase details +AFPurchaseDetails purchaseDetails = AFPurchaseDetails( + purchaseType: AFPurchaseType.oneTimePurchase, + purchaseToken: "your_purchase_token", + productId: "your_product_id", +); + +// Validate purchase +try { + Map result = await appsFlyerSdk.validateAndLogInAppPurchaseV2( + purchaseDetails, + additionalParameters: {"custom_param": "value"} + ); + print("Validation successful: $result"); +} catch (e) { + print("Validation failed: $e"); +} +``` + +--- + +***Legacy APIs:*** ***Android:*** @@ -572,6 +623,67 @@ appsflyerSdk.onPurchaseValidation((res){ }); ``` +--- + +##### **validateAndLogInAppPurchaseV2 (Recommended - BETA)** + +> âš ī¸ **BETA Feature**: This API is currently in beta. While it's stable and recommended for new implementations, please test thoroughly in your environment before production use. + +**`Future> validateAndLogInAppPurchaseV2(AFPurchaseDetails purchaseDetails, {Map? additionalParameters})`** + +The unified cross-platform purchase validation API introduced in SDK v6.17.3. This is the recommended approach for validating in-app purchases as it provides a consistent interface across Android and iOS. + +|| parameter | type | description | +|| --------- | ----- | ----------- | +|| `purchaseDetails` | `AFPurchaseDetails` | Purchase details object containing purchase type, token, and product ID | +|| `additionalParameters` | `Map?` | Optional additional parameters to send with the validation request | + +**Returns:** `Future>` - Validation result with detailed response information + +**AFPurchaseDetails Properties:** + +|| property | type | description | +|| -------- | ----- | ----------- | +|| `purchaseType` | `AFPurchaseType` | Type of purchase (`AFPurchaseType.oneTimePurchase` or `AFPurchaseType.subscription`) | +|| `purchaseToken` | `String` | Purchase token obtained from the app store | +|| `productId` | `String` | Product identifier of the purchased item | + +_Example:_ + +```dart +// Create purchase details +AFPurchaseDetails purchaseDetails = AFPurchaseDetails( + purchaseType: AFPurchaseType.subscription, + purchaseToken: "your_purchase_token_from_store", + productId: "premium_subscription_monthly", +); + +// Validate the purchase +try { + Map validationResult = await appsflyerSdk.validateAndLogInAppPurchaseV2( + purchaseDetails, + additionalParameters: { + "app_version": "1.2.0", + "validation_source": "flutter_example" + } + ); + + print("✅ Purchase validation successful!"); + print("Validation result: $validationResult"); + +} catch (e) { + print("❌ Purchase validation failed: $e"); + // Handle validation error +} +``` + +**Key Benefits:** +- **Cross-platform compatibility**: Works on both Android and iOS with the same API +- **Type safety**: Uses structured data classes instead of platform-specific parameters +- **Enhanced error handling**: Provides detailed error information in structured format +- **Future-proof**: Built on AppsFlyer's latest V2 validation infrastructure +- **Automatic routing**: Automatically routes to correct validation endpoints based on purchase type + --- ## ** `void sendPushNotificationData(Map? userInfo)`** diff --git a/doc/AdvancedAPI.md b/doc/AdvancedAPI.md index e41720b9..d46b8dd8 100644 --- a/doc/AdvancedAPI.md +++ b/doc/AdvancedAPI.md @@ -114,7 +114,58 @@ appsFlyerSdk.generateInviteLink(inviteLinkParams, Receipt validation is a secure mechanism whereby the payment platform (e.g. Apple or Google) validates that an in-app purchase indeed occurred as reported.
Learn more - https://support.appsflyer.com/hc/en-us/articles/207032106-Receipt-validation-for-in-app-purchases
-There are two different functions, one for iOS and one for Android: +**Cross-Platform V2 API (Recommended - SDK v6.17.3+) - BETA:** + +> âš ī¸ **BETA Feature**: This API is currently in beta. While it's stable and recommended for new implementations, please test thoroughly in your environment before production use. + +The new unified purchase validation API that works across both Android and iOS platforms: + +```dart +Future> validateAndLogInAppPurchaseV2( + AFPurchaseDetails purchaseDetails, + {Map? additionalParameters}) +``` + +**AFPurchaseDetails class:** +```dart +AFPurchaseDetails( + purchaseType: AFPurchaseType, // oneTimePurchase or subscription + purchaseToken: String, // Purchase token from app store + productId: String, // Product identifier +) +``` + +Example: +```dart +// Create purchase details +AFPurchaseDetails purchaseDetails = AFPurchaseDetails( + purchaseType: AFPurchaseType.oneTimePurchase, + purchaseToken: "sample_purchase_token_12345", + productId: "com.example.product", +); + +// Validate purchase (works on both Android and iOS) +try { + Map result = await appsFlyerSdk.validateAndLogInAppPurchaseV2( + purchaseDetails, + additionalParameters: {"custom_param": "value"} + ); + print("Validation successful: $result"); +} catch (e) { + print("Validation failed: $e"); +} +``` + +**Benefits of V2 API:** +- ✅ **Cross-platform**: Single API works on both Android and iOS +- ✅ **Type-safe**: Uses structured data classes instead of raw strings +- ✅ **Better error handling**: Returns structured error information +- ✅ **Enhanced validation**: Uses AppsFlyer's latest validation infrastructure +- ✅ **Future-proof**: Built for AppsFlyer's V2 validation endpoints + +--- + +**Legacy Platform-Specific APIs:** **Android:** ```dart diff --git a/doc/PurchaseConnector.md b/doc/PurchaseConnector.md index e076ebee..b4ef8f7e 100644 --- a/doc/PurchaseConnector.md +++ b/doc/PurchaseConnector.md @@ -24,6 +24,10 @@ support@appsflyer.com - [Stop Observing Transactions](#stop) - [Log Subscriptions](#log-subscriptions) - [Log In App Purchases](#log-inapps) +* [StoreKit Version Configuration (iOS)](#storekit-configuration) + - [Available StoreKit Versions](#storekit-versions) + - [Configuration Examples](#storekit-examples) + - [StoreKit 2 Benefits and Requirements](#storekit-notes) * [Register Validation Results Listeners](#validation-callbacks) - [Cross-Platform Considerations](#cross-platform-considerations) - [Android Callback Types](#android-callback-types) @@ -85,6 +89,7 @@ To properly set up the configuration object, you must specify certain parameters - `logSubscriptions`: If set to `true`, the connector logs all subscription events. - `logInApps`: If set to `true`, the connector logs all in-app purchase events. - `sandbox`: If set to `true`, transactions are tested in a sandbox environment. Be sure to set this to `false` in production. +- `storeKitVersion`: (iOS only) Specifies which StoreKit version to use. Defaults to `StoreKitVersion.storeKit1` if not specified. Here's an example usage: @@ -95,6 +100,7 @@ void main() { logSubscriptions: true, // Enables logging of subscription events logInApps: true, // Enables logging of in-app purchase events sandbox: true, // Enables testing in a sandbox environment + storeKitVersion: StoreKitVersion.storeKit1, // iOS only: StoreKit version (defaults to storeKit1) ), ); @@ -114,6 +120,7 @@ void main() { logSubscriptions: true, logInApps: true, sandbox: true, + storeKitVersion: StoreKitVersion.storeKit1, // Default StoreKit version ), ); @@ -124,6 +131,7 @@ void main() { logSubscriptions: false, logInApps: false, sandbox: false, + storeKitVersion: StoreKitVersion.storeKit2, // This will be ignored ), ); @@ -178,6 +186,89 @@ final afPurchaseClient = PurchaseConnector( config: PurchaseConnectorConfiguration(logInApps: true)); ``` +##
StoreKit Version Configuration (iOS) + +The Purchase Connector supports both StoreKit 1 and StoreKit 2 on iOS. You can configure which version to use via the `storeKitVersion` parameter in `PurchaseConnectorConfiguration`. + +### Available StoreKit Versions + +- **`StoreKitVersion.storeKit1`** (Default) - Uses the original StoreKit framework +- **`StoreKitVersion.storeKit2`** - Uses the modern StoreKit 2 framework (iOS 15.0+) + +### Configuration Examples + +**Using StoreKit 1 (Default):** +```dart +final afPurchaseClient = PurchaseConnector( + config: PurchaseConnectorConfiguration( + logSubscriptions: true, + logInApps: true, + sandbox: true, + // StoreKit 1 is used by default, no need to specify + ), +); +``` + +**Explicitly Using StoreKit 1:** +```dart +final afPurchaseClient = PurchaseConnector( + config: PurchaseConnectorConfiguration( + logSubscriptions: true, + logInApps: true, + sandbox: true, + storeKitVersion: StoreKitVersion.storeKit1, // Explicitly set to StoreKit 1 + ), +); +``` + +**Using StoreKit 2:** +```dart +final afPurchaseClient = PurchaseConnector( + config: PurchaseConnectorConfiguration( + logSubscriptions: true, + logInApps: true, + sandbox: true, + storeKitVersion: StoreKitVersion.storeKit2, // Use modern StoreKit 2 + ), +); +``` + +### StoreKit 2 Benefits and Requirements + +**Benefits of StoreKit 2:** +- ✅ **Modern API**: Built with Swift's async/await patterns +- ✅ **Better Performance**: More efficient transaction processing +- ✅ **Enhanced Features**: Improved subscription management and transaction handling +- ✅ **Future-Proof**: Apple's recommended approach for new apps + +**Requirements:** +- 📱 **iOS 15.0+**: StoreKit 2 requires iOS 15.0 or later +- 🔄 **Backward Compatibility**: Falls back to StoreKit 1 on older iOS versions automatically +- đŸ§Ē **Testing**: Thoroughly test on your target iOS versions + +**Example with Error Handling:** +```dart +try { + final afPurchaseClient = PurchaseConnector( + config: PurchaseConnectorConfiguration( + logSubscriptions: true, + logInApps: true, + sandbox: true, + storeKitVersion: StoreKitVersion.storeKit2, + ), + ); + + // Start observing transactions + afPurchaseClient.startObservingTransactions(); + + print("Purchase Connector initialized with StoreKit 2"); +} catch (e) { + print("Failed to initialize Purchase Connector: $e"); + // Consider fallback to StoreKit 1 or handle error appropriately +} +``` + +> 📝 **Note**: If you don't specify `storeKitVersion`, the connector defaults to `StoreKitVersion.storeKit1` for maximum compatibility. Only use StoreKit 2 if your app's minimum iOS version is 15.0 or higher, or if you've implemented proper fallback handling. ## Register Validation Results Listeners You can register listeners to get the validation results once getting a response from AppsFlyer servers to let you know if the purchase was validated successfully.
@@ -245,6 +336,22 @@ Remember to switch the environment back to production (set `sandbox` to `false`) To test purchases in an iOS environment on a real device with a TestFlight sandbox account, you also need to set `sandbox` to `true`. +**StoreKit Version Considerations for Testing:** +- **StoreKit 1**: Works on all iOS versions, well-established testing procedures +- **StoreKit 2**: Requires iOS 15.0+, provides enhanced testing capabilities and more detailed transaction information + +```dart +// Example configuration for testing with StoreKit 2 +final purchaseConnector = PurchaseConnector( + config: PurchaseConnectorConfiguration( + sandbox: true, // Enable sandbox for testing + storeKitVersion: StoreKitVersion.storeKit2, // Use StoreKit 2 for enhanced testing + logSubscriptions: true, + logInApps: true, + ), +); +``` + > *IMPORTANT NOTE: Before releasing your app to production please be sure to set `sandbox` to `false`. If a production purchase event is sent in sandbox mode, your event will not be validated properly! * ###
Dart Usage for Android and iOS @@ -252,9 +359,24 @@ To test purchases in an iOS environment on a real device with a TestFlight sandb For both Android and iOS, you can set the sandbox environment using the `sandbox` parameter in the `PurchaseConnectorConfiguration` when you instantiate `PurchaseConnector` in your Dart code like this: ```dart -// Testing in a sandbox environment +// Testing in a sandbox environment with StoreKit 1 (default) final purchaseConnector = PurchaseConnector( - PurchaseConnectorConfiguration(sandbox: true) + config: PurchaseConnectorConfiguration( + sandbox: true, + logSubscriptions: true, + logInApps: true, + // storeKitVersion defaults to StoreKitVersion.storeKit1 + ) +); + +// Testing in a sandbox environment with StoreKit 2 (iOS 15.0+) +final purchaseConnectorSK2 = PurchaseConnector( + config: PurchaseConnectorConfiguration( + sandbox: true, + logSubscriptions: true, + logInApps: true, + storeKitVersion: StoreKitVersion.storeKit2, // Enhanced testing capabilities + ) ); ``` @@ -275,7 +397,11 @@ Add following keep rules to your `proguard-rules.pro` file: ## Full Code Example ```dart PurchaseConnectorConfiguration config = PurchaseConnectorConfiguration( - logSubscriptions: true, logInApps: true, sandbox: false); + logSubscriptions: true, + logInApps: true, + sandbox: false, + storeKitVersion: StoreKitVersion.storeKit2 // Use StoreKit 2 on iOS (requires iOS 15.0+) +); final afPurchaseClient = PurchaseConnector(config: config); // set listeners for Android diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 15cada48..d795332e 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -5,6 +5,24 @@ + + + + + + + + + + Function(String, Map) logEvent; final void Function() logAdRevenueEvent; + final Future?> Function(String, String) validatePurchase; Object deepLinkData; HomeContainer({ @@ -15,6 +16,7 @@ class HomeContainer extends StatefulWidget { required this.deepLinkData, required this.logEvent, required this.logAdRevenueEvent, + required this.validatePurchase, }); @override @@ -32,6 +34,20 @@ class _HomeContainerState extends State { String _logEventResponse = "Awaiting event status"; + // Purchase validation fields + final TextEditingController _purchaseTokenController = + TextEditingController(text: "sample_purchase_token_12345"); + final TextEditingController _productIdController = + TextEditingController(text: "com.example.product"); + String _validationResponse = "Awaiting validation"; + + @override + void dispose() { + _purchaseTokenController.dispose(); + _productIdController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return SingleChildScrollView( @@ -40,55 +56,50 @@ class _HomeContainerState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - padding: EdgeInsets.all(20.0), - decoration: BoxDecoration( - color: Colors.white, - border: Border.all( - color: Colors.blueGrey, - width: 0.5 - ), - borderRadius: BorderRadius.circular(5), - ),child: Column( - children: [ - Text( - "APPSFLYER SDK", - style: TextStyle( - fontSize: 18, - color: Colors.black, - fontWeight: FontWeight.w500, - ), - ), - SizedBox(height: AppConstants.TOP_PADDING), - TextBorder( - controller: TextEditingController( - text: widget.onData.isNotEmpty - ? Utils.formatJson(widget.onData) - : "Waiting for conversion data...", + Container( + padding: EdgeInsets.all(20.0), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.blueGrey, width: 0.5), + borderRadius: BorderRadius.circular(5), ), - labelText: "CONVERSION DATA", - ), - SizedBox(height: 12.0), - TextBorder( - controller: TextEditingController( - text: widget.deepLinkData != null - ? Utils.formatJson(widget.deepLinkData) - : "Waiting for attribution data...", + child: Column( + children: [ + Text( + "APPSFLYER SDK", + style: TextStyle( + fontSize: 18, + color: Colors.black, + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: AppConstants.TOP_PADDING), + TextBorder( + controller: TextEditingController( + text: widget.onData.isNotEmpty + ? Utils.formatJson(widget.onData) + : "Waiting for conversion data...", + ), + labelText: "CONVERSION DATA", + ), + SizedBox(height: 12.0), + TextBorder( + controller: TextEditingController( + text: widget.deepLinkData != null + ? Utils.formatJson(widget.deepLinkData) + : "Waiting for attribution data...", + ), + labelText: "ATTRIBUTION DATA", + ), + ], ), - labelText: "ATTRIBUTION DATA", ), - ], - ), - ), SizedBox(height: 12.0), Container( padding: EdgeInsets.all(20.0), decoration: BoxDecoration( color: Colors.white, - border: Border.all( - color: Colors.blueGrey, - width: 0.5 - ), + border: Border.all(color: Colors.blueGrey, width: 0.5), borderRadius: BorderRadius.circular(5), ), child: Column( @@ -104,23 +115,22 @@ class _HomeContainerState extends State { SizedBox(height: 12.0), TextBorder( controller: TextEditingController( - text: "Event Name: $eventName\nEvent Values: $eventValues" - ), + text: + "Event Name: $eventName\nEvent Values: $eventValues"), labelText: "EVENT REQUEST", ), SizedBox(height: 12.0), TextBorder( labelText: "SERVER RESPONSE", - controller: TextEditingController( - text: _logEventResponse - ), + controller: TextEditingController(text: _logEventResponse), ), SizedBox(height: 20.0), ElevatedButton( onPressed: () { widget.logEvent(eventName, eventValues).then((onValue) { setState(() { - _logEventResponse = "Event Status: " + onValue.toString(); + _logEventResponse = + "Event Status: " + onValue.toString(); }); }).catchError((onError) { setState(() { @@ -131,7 +141,8 @@ class _HomeContainerState extends State { child: Text("Trigger Purchase Event"), style: ElevatedButton.styleFrom( backgroundColor: Colors.white, - padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10), + padding: + EdgeInsets.symmetric(horizontal: 20, vertical: 10), textStyle: TextStyle( fontWeight: FontWeight.bold, fontSize: 16, @@ -145,7 +156,97 @@ class _HomeContainerState extends State { child: Text("Trigger AdRevenue Event"), style: ElevatedButton.styleFrom( backgroundColor: Colors.white, - padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10), + padding: + EdgeInsets.symmetric(horizontal: 20, vertical: 10), + textStyle: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ), + ], + ), + ), + SizedBox(height: 12.0), + Container( + padding: EdgeInsets.all(20.0), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.blueGrey, width: 0.5), + borderRadius: BorderRadius.circular(5), + ), + child: Column( + children: [ + Text( + "PURCHASE VALIDATION V2", + style: TextStyle( + fontSize: 18, + color: Colors.black, + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 12.0), + TextFormField( + controller: _purchaseTokenController, + decoration: InputDecoration( + labelText: "Purchase Token", + border: OutlineInputBorder(), + contentPadding: + EdgeInsets.symmetric(horizontal: 12, vertical: 8), + ), + maxLines: 2, + ), + SizedBox(height: 12.0), + TextFormField( + controller: _productIdController, + decoration: InputDecoration( + labelText: "Product ID", + border: OutlineInputBorder(), + contentPadding: + EdgeInsets.symmetric(horizontal: 12, vertical: 8), + ), + ), + SizedBox(height: 12.0), + TextBorder( + labelText: "VALIDATION RESPONSE", + controller: + TextEditingController(text: _validationResponse), + ), + SizedBox(height: 20.0), + ElevatedButton( + onPressed: () { + final purchaseToken = + _purchaseTokenController.text.trim(); + final productId = _productIdController.text.trim(); + + if (purchaseToken.isEmpty || productId.isEmpty) { + setState(() { + _validationResponse = + "Error: Purchase token and product ID are required"; + }); + return; + } + + widget + .validatePurchase(purchaseToken, productId) + .then((result) { + setState(() { + _validationResponse = + "Validation successful!\nResult: ${Utils.formatJson(result ?? {})}"; + }); + }).catchError((error) { + setState(() { + _validationResponse = + "Validation failed!\nError: $error"; + }); + }); + }, + child: Text("Validate Purchase V2"), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + padding: + EdgeInsets.symmetric(horizontal: 20, vertical: 10), textStyle: TextStyle( fontWeight: FontWeight.bold, fontSize: 16, @@ -160,4 +261,4 @@ class _HomeContainerState extends State { ), ); } -} \ No newline at end of file +} diff --git a/example/lib/main_page.dart b/example/lib/main_page.dart index bb55c544..3cc37d42 100644 --- a/example/lib/main_page.dart +++ b/example/lib/main_page.dart @@ -125,6 +125,7 @@ class MainPageState extends State { deepLinkData: _deepLinkData, logEvent: logEvent, logAdRevenueEvent: logAdRevenueEvent, + validatePurchase: validatePurchase, ), ), ElevatedButton( @@ -180,6 +181,36 @@ class MainPageState extends State { } } + Future?> validatePurchase( + String purchaseToken, String productId) async { + try { + // Create purchase details + final purchaseDetails = AFPurchaseDetails( + purchaseType: AFPurchaseType.oneTimePurchase, + purchaseToken: purchaseToken, + productId: productId, + ); + + // Additional parameters (optional) + Map additionalParameters = { + 'validation_source': 'flutter_example', + 'app_version': '1.0.0', + }; + + // Validate the purchase + final result = await _appsflyerSdk.validateAndLogInAppPurchaseV2( + purchaseDetails, + additionalParameters: additionalParameters, + ); + + print("Purchase validation successful: $result"); + return result as Map?; + } catch (e) { + print("Purchase validation failed: $e"); + rethrow; + } + } + void showMessage(String message) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text(message), diff --git a/ios/Classes/AppsflyerSdkPlugin.h b/ios/Classes/AppsflyerSdkPlugin.h index b7a9b8d3..4a1c7f01 100644 --- a/ios/Classes/AppsflyerSdkPlugin.h +++ b/ios/Classes/AppsflyerSdkPlugin.h @@ -18,7 +18,7 @@ @end // Appsflyer JS objects -#define kAppsFlyerPluginVersion @"6.17.1" +#define kAppsFlyerPluginVersion @"6.17.3" #define afDevKey @"afDevKey" #define afAppId @"afAppId" #define afIsDebug @"isDebug" diff --git a/ios/Classes/AppsflyerSdkPlugin.m b/ios/Classes/AppsflyerSdkPlugin.m index 463ab9fc..d4dacacb 100644 --- a/ios/Classes/AppsflyerSdkPlugin.m +++ b/ios/Classes/AppsflyerSdkPlugin.m @@ -116,6 +116,8 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { [self setAdditionalData:call result:result]; }else if([@"validateAndLogInAppIosPurchase" isEqualToString:call.method]){ [self validateAndLogInAppPurchase:call result:result]; + }else if([@"validateAndLogInAppPurchaseV2" isEqualToString:call.method]){ + [self validateAndLogInAppPurchaseV2:call result:result]; }else if([@"getAppsFlyerUID" isEqualToString:call.method]){ [self getAppsFlyerUID:result]; }else if([@"setSharingFilter" isEqualToString:call.method]){ @@ -636,6 +638,90 @@ - (void)validateAndLogInAppPurchase:(FlutterMethodCall*)call result:(FlutterResu result(nil); } +- (void)validateAndLogInAppPurchaseV2:(FlutterMethodCall*)call result:(FlutterResult)result{ + @try { + // Extract purchase details map from Flutter + NSDictionary* purchaseDetailsMap = call.arguments[@"purchaseDetails"]; + NSDictionary* additionalParameters = call.arguments[@"additionalParameters"]; + + if (purchaseDetailsMap == nil) { + result([FlutterError errorWithCode:@"INVALID_ARGUMENTS" + message:@"Purchase details cannot be null" + details:nil]); + return; + } + + // Extract individual fields from purchase details map + NSString* purchaseTypeString = purchaseDetailsMap[@"purchaseType"]; + NSString* purchaseToken = purchaseDetailsMap[@"purchaseToken"]; + NSString* productId = purchaseDetailsMap[@"productId"]; + + // Validate required fields + if (purchaseTypeString == nil || purchaseToken == nil || productId == nil) { + result([FlutterError errorWithCode:@"INVALID_ARGUMENTS" + message:@"Purchase details must contain purchaseType, purchaseToken, and productId" + details:nil]); + return; + } + + // Map Dart enum values to iOS purchase type + // For iOS, we use transactionId instead of purchaseToken, so we'll use purchaseToken as transactionId + NSString* transactionId = purchaseToken; + + NSLog(@"AppsFlyer Debug: validateAndLogInAppPurchaseV2 called with purchaseType: %@, transactionId: %@, productId: %@", purchaseTypeString, transactionId, productId); + + // Call the actual AppsFlyer iOS V2 API + [self callAppsFlyerV2API:purchaseTypeString + transactionId:transactionId + productId:productId + additionalParameters:additionalParameters + result:result]; + + } @catch (NSException *exception) { + NSLog(@"AppsFlyer: Error in validateAndLogInAppPurchaseV2: %@", exception.reason); + result([FlutterError errorWithCode:@"VALIDATION_ERROR" + message:[NSString stringWithFormat:@"Purchase validation failed: %@", exception.reason] + details:nil]); + } +} + +- (void)callAppsFlyerV2API:(NSString*)purchaseTypeString + transactionId:(NSString*)transactionId + productId:(NSString*)productId + additionalParameters:(NSDictionary*)additionalParameters + result:(FlutterResult)result { + + [[AppsFlyerLib shared] validateAndLogInAppPurchase:productId + price:nil // V2 doesn't use price + currency:nil // V2 doesn't use currency + transactionId:transactionId + additionalParameters:additionalParameters + success:^(NSDictionary *response) { + NSLog(@"AppsFlyer Debug: validateAndLogInAppPurchaseV2 Success!"); + // V2 API returns response directly without wrapper + NSMutableDictionary *v2Response = [NSMutableDictionary dictionaryWithDictionary:response]; + v2Response[@"purchase_type"] = purchaseTypeString; + result(v2Response); + } + failure:^(NSError *error, id errorResponse) { + NSLog(@"AppsFlyer Debug: validateAndLogInAppPurchaseV2 failed with Error: %@", error); + + // Create error response for V2 format + NSMutableDictionary *errorData = [NSMutableDictionary dictionary]; + if (error) { + errorData[@"error_message"] = error.localizedDescription ?: @"Purchase validation failed"; + errorData[@"error_code"] = @(error.code); + } + if (errorResponse && [errorResponse isKindOfClass:[NSDictionary class]]) { + [errorData addEntriesFromDictionary:(NSDictionary*)errorResponse]; + } + + result([FlutterError errorWithCode:@"VALIDATION_ERROR" + message:error.localizedDescription ?: @"Purchase validation failed" + details:errorData]); + }]; +} + - (void)onValidateSuccess: (NSDictionary*) data{ [_streamHandler sendResponseToFlutter:afValidatePurchase status:afSuccess data:data]; } diff --git a/ios/PurchaseConnector/PurchaseConnectorPlugin.swift b/ios/PurchaseConnector/PurchaseConnectorPlugin.swift index 403f05e6..82ef7772 100644 --- a/ios/PurchaseConnector/PurchaseConnectorPlugin.swift +++ b/ios/PurchaseConnector/PurchaseConnectorPlugin.swift @@ -31,7 +31,7 @@ import Flutter private let logSubscriptionsKey = "logSubscriptionPurchase" private let logInAppsKey = "logInAppPurchase" private let sandboxKey = "sandbox" - + private let storeKitVersionKey = "storeKitVersion" /// Private constructor, used to prevent direct instantiation of this class and ensure singleton behaviour. private override init() {} @@ -74,7 +74,8 @@ import Flutter let logSubscriptions = arguments?[logSubscriptionsKey] as? Bool ?? false let logInApps = arguments?[logInAppsKey] as? Bool ?? false let sandbox = arguments?[sandboxKey] as? Bool ?? false - + let storeKitVersion = arguments?[storeKitVersionKey] as? Int ?? 0 // 0 for StoreKit V1, 1 for StoreKit V2 + /// Define an options variable to manage enabled options. var options: AutoLogPurchaseRevenueOptions = [] @@ -91,7 +92,17 @@ import Flutter logOptions = options connector!.isSandbox = sandbox connector!.purchaseRevenueDelegate = self - + // Set StoreKit version based on the configuration + if storeKitVersion == 1 { + if #available(iOS 15.0, *) { + connector!.setStoreKitVersion(.SK2) + } else { + print("[AppsFlyer_purchase] iOS: StoreKit 2 requested but iOS < 15.0, falling back to StoreKit 1") + connector!.setStoreKitVersion(.SK1) + } + } else { + connector!.setStoreKitVersion(.SK1) + } /// Report a successful operation back to Dart. result(nil) } diff --git a/ios/appsflyer_sdk.podspec b/ios/appsflyer_sdk.podspec index 00912f5d..b9f1d580 100644 --- a/ios/appsflyer_sdk.podspec +++ b/ios/appsflyer_sdk.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'appsflyer_sdk' - s.version = '6.17.1' + s.version = '6.17.3' s.summary = 'AppsFlyer Integration for Flutter' s.description = 'AppsFlyer is the market leader in mobile advertising attribution & analytics, helping marketers to pinpoint their targeting, optimize their ad spend and boost their ROI.' s.homepage = 'https://github.com/AppsFlyerSDK/flutter_appsflyer_sdk' @@ -21,12 +21,12 @@ Pod::Spec.new do |s| ss.source_files = 'Classes/**/*' ss.public_header_files = 'Classes/**/*.h' ss.dependency 'Flutter' - ss.ios.dependency 'AppsFlyerFramework','6.17.1' + ss.ios.dependency 'AppsFlyerFramework','6.17.3' end s.subspec 'PurchaseConnector' do |ss| ss.dependency 'Flutter' - ss.ios.dependency 'PurchaseConnector', '6.17.1' + ss.ios.dependency 'PurchaseConnector', '6.17.3' ss.source_files = 'PurchaseConnector/**/*' ss.public_header_files = 'PurchaseConnector/**/*.h' diff --git a/lib/appsflyer_sdk.dart b/lib/appsflyer_sdk.dart index ff9df5ce..99a0af6b 100644 --- a/lib/appsflyer_sdk.dart +++ b/lib/appsflyer_sdk.dart @@ -21,6 +21,7 @@ part 'src/purchase_connector/purchase_connector.dart'; part 'src/purchase_connector/connector_callbacks.dart'; part 'src/purchase_connector/missing_configuration_exception.dart'; part 'src/purchase_connector/purchase_connector_configuration.dart'; +part 'src/purchase_connector/store_kit_version.dart'; part 'src/purchase_connector/models/subscription_purchase.dart'; part 'src/purchase_connector/models/in_app_purchase_validation_result.dart'; part 'src/purchase_connector/models/product_purchase.dart'; @@ -32,3 +33,4 @@ part 'src/appsflyer_consent.dart'; part 'src/appsflyer_request_listener.dart'; part 'appsflyer_sdk.g.dart'; part 'src/appsflyer_ad_revenue_data.dart'; +part 'src/af_purchase_details.dart'; diff --git a/lib/src/af_purchase_details.dart b/lib/src/af_purchase_details.dart new file mode 100644 index 00000000..a84c2e04 --- /dev/null +++ b/lib/src/af_purchase_details.dart @@ -0,0 +1,41 @@ +part of appsflyer_sdk; + +/// Enum representing the type of purchase for AppsFlyer validation. +enum AFPurchaseType { + oneTimePurchase, + subscription, +} + +/// Data class representing purchase details for AppsFlyer validation. +/// +/// This class encapsulates the essential information needed to validate +/// in-app purchases with AppsFlyer's validation API. +@immutable +class AFPurchaseDetails { + final AFPurchaseType purchaseType; + final String purchaseToken; + final String productId; + + /// Creates an [AFPurchaseDetails] instance. + /// + /// All parameters are required: + /// - [purchaseType]: The type of purchase being validated + /// - [purchaseToken]: The token provided by the app store for this purchase + /// - [productId]: The identifier of the product that was purchased + const AFPurchaseDetails({ + required this.purchaseType, + required this.purchaseToken, + required this.productId, + }); + + /// Converts the purchase details to a map for method channel communication. + Map toMap() { + return { + 'purchaseType': purchaseType == AFPurchaseType.oneTimePurchase + ? 'one_time_purchase' + : 'subscription', + 'purchaseToken': purchaseToken, + 'productId': productId, + }; + } +} diff --git a/lib/src/appsflyer_constants.dart b/lib/src/appsflyer_constants.dart index 0d9abb4a..e4ea22bf 100644 --- a/lib/src/appsflyer_constants.dart +++ b/lib/src/appsflyer_constants.dart @@ -3,7 +3,7 @@ part of appsflyer_sdk; enum EmailCryptType { EmailCryptTypeNone, EmailCryptTypeSHA256 } class AppsflyerConstants { - static const String PLUGIN_VERSION = "6.17.1"; + static const String PLUGIN_VERSION = "6.17.3"; static const String AF_DEV_KEY = "afDevKey"; static const String AF_APP_Id = "afAppId"; static const String AF_IS_DEBUG = "isDebug"; @@ -38,7 +38,7 @@ class AppsflyerConstants { static const String VALIDATION_INFO = "validationInfo"; static const String ERROR = "error"; static const String RESULT = "result"; - + static const String STORE_KIT_VERSION_KEY = "storeKitVersion"; // Purchase Connector listeners static const String SUBSCRIPTION_PURCHASE_VALIDATION_RESULT_LISTENER_ON_RESPONSE = diff --git a/lib/src/appsflyer_sdk.dart b/lib/src/appsflyer_sdk.dart index a7a6a793..5336fa44 100644 --- a/lib/src/appsflyer_sdk.dart +++ b/lib/src/appsflyer_sdk.dart @@ -438,6 +438,26 @@ class AppsflyerSdk { }); } + /// Validates and logs in-app purchases using the new AppsFlyer validation API (V2). + /// This method validates a purchase using the [AFPurchaseDetails] object. + /// + /// [purchaseDetails] - The purchase details containing type, token, and product ID + /// [additionalParameters] - Optional additional parameters to send with the validation request + /// + /// Returns a Future that completes with the validation result or throws an error if validation fails. + Future> validateAndLogInAppPurchaseV2( + AFPurchaseDetails purchaseDetails, + {Map? additionalParameters}) async { + final arguments = { + 'purchaseDetails': purchaseDetails.toMap(), + 'additionalParameters': additionalParameters, + }; + + final result = await _methodChannel.invokeMethod( + "validateAndLogInAppPurchaseV2", arguments); + return Map.from(result); + } + /// set sandbox for iOS purchase validation void useReceiptValidationSandbox(bool isSandboxEnabled) { _methodChannel.invokeMethod( diff --git a/lib/src/purchase_connector/purchase_connector.dart b/lib/src/purchase_connector/purchase_connector.dart index f1ebc0c0..ee740d8b 100644 --- a/lib/src/purchase_connector/purchase_connector.dart +++ b/lib/src/purchase_connector/purchase_connector.dart @@ -74,6 +74,7 @@ class _PurchaseConnectorImpl implements PurchaseConnector { AppsflyerConstants.LOG_SUBS_KEY: config.logSubscriptions, AppsflyerConstants.LOG_IN_APP_KEY: config.logInApps, AppsflyerConstants.SANDBOX_KEY: config.sandbox, + AppsflyerConstants.STORE_KIT_VERSION_KEY: config.storeKitVersion.value, }; print("[AppsFlyer_PC_Debug] Sending config to native: $configMap"); diff --git a/lib/src/purchase_connector/purchase_connector_configuration.dart b/lib/src/purchase_connector/purchase_connector_configuration.dart index 17551c7f..80125748 100644 --- a/lib/src/purchase_connector/purchase_connector_configuration.dart +++ b/lib/src/purchase_connector/purchase_connector_configuration.dart @@ -9,8 +9,17 @@ class PurchaseConnectorConfiguration { bool logInApps; bool sandbox; - PurchaseConnectorConfiguration( - {this.logSubscriptions = false, - this.logInApps = false, - this.sandbox = false}); + /// The StoreKit version to use on iOS. + /// + /// - [StoreKitVersion.SK1]: Use StoreKit 1 (legacy, compatible with all iOS versions) + /// - [StoreKitVersion.SK2]: Use StoreKit 2 (modern, iOS 15+ only, better performance) + StoreKitVersion storeKitVersion; + + PurchaseConnectorConfiguration({ + this.logSubscriptions = false, + this.logInApps = false, + this.sandbox = false, + this.storeKitVersion = + StoreKitVersion.SK1, // Default to SK1 for backwards compatibility + }); } diff --git a/lib/src/purchase_connector/store_kit_version.dart b/lib/src/purchase_connector/store_kit_version.dart new file mode 100644 index 00000000..f6971764 --- /dev/null +++ b/lib/src/purchase_connector/store_kit_version.dart @@ -0,0 +1,51 @@ +part of appsflyer_sdk; + +/// Enum representing StoreKit versions for iOS Purchase Connector configuration. +/// +/// StoreKit is Apple's framework for handling in-app purchases and subscriptions. +/// Different versions offer different capabilities and performance characteristics. +enum StoreKitVersion { + /// StoreKit 1 (Legacy) + /// + /// The original StoreKit framework, available on all iOS versions. + /// Use this for compatibility with older iOS versions or when you need + /// specific StoreKit 1 features. + SK1, + + /// StoreKit 2 (Modern) + /// + /// The newer StoreKit framework, available on iOS 15.0 and later. + /// Offers improved performance, better transaction handling, and + /// more comprehensive purchase validation features. + /// Recommended for apps targeting iOS 15+ for better automatic purchase tracking. + SK2; + + /// Returns the integer value associated with this StoreKit version. + /// + /// This value is used internally by the native iOS implementation + /// to configure the appropriate StoreKit version. + int get value { + switch (this) { + case StoreKitVersion.SK1: + return 0; + case StoreKitVersion.SK2: + return 1; + } + } + + /// Creates a StoreKitVersion from an integer value. + /// + /// Returns [StoreKitVersion.SK1] for value 0, [StoreKitVersion.SK2] for value 1. + /// Defaults to [StoreKitVersion.SK1] for any other value. + static StoreKitVersion fromValue(int value) { + switch (value) { + case 0: + return StoreKitVersion.SK1; + case 1: + return StoreKitVersion.SK2; + default: + return StoreKitVersion + .SK1; // Default to SK1 for backwards compatibility + } + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 1aaa948c..64d27242 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: appsflyer_sdk description: A Flutter plugin for AppsFlyer SDK. Supports iOS and Android. -version: 6.17.1 +version: 6.17.3 homepage: https://github.com/AppsFlyerSDK/flutter_appsflyer_sdk From 8b923b127a8552078b1d527cc4d554be46a772a4 Mon Sep 17 00:00:00 2001 From: Dani Koza <103039399+Dani-Koza-AF@users.noreply.github.com> Date: Mon, 29 Sep 2025 15:16:50 +0300 Subject: [PATCH 30/42] Added an important note to deep-links docs (#416) --- doc/DeepLink.md | 23 ++++++++++++++++++++++- doc/Guides.md | 22 ++++++++++++++++++++++ doc/PurchaseConnector.md | 2 +- 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/doc/DeepLink.md b/doc/DeepLink.md index a356e183..28f9a990 100644 --- a/doc/DeepLink.md +++ b/doc/DeepLink.md @@ -1,8 +1,29 @@ # Deep linking +> âš ī¸ **IMPORTANT: Flutter 3.27+ Breaking Change** +> +> Starting from Flutter 3.27, the default value for Flutter's deep linking option has changed from `false` to `true`. This means Flutter's built-in deep linking is now enabled by default, which can conflict with third-party deep linking plugins like AppsFlyer. +> +> **If you're using Flutter 3.27 or higher, you MUST disable Flutter's built-in deep linking** by adding the following configurations: +> +> **Android** - Add to your `AndroidManifest.xml` inside the `` tag: +> +> ```xml +> +> ``` +> +> **iOS** - Add to your `Info.plist` file: +> +> ```xml +> FlutterDeepLinkingEnabled +> +> ``` +> +> For more details, see the [official Flutter documentation](https://docs.flutter.dev/release/breaking-changes/deep-links-flag-change). + Deep Linking vs Deferred Deep Linking: -A deep link is a special URL that routes to a specific spot, whether that’s on a website or in an app. A “mobile deep link” then, is a link that contains all the information needed to take a user directly into an app or a particular location within an app instead of just launching the app’s home page. +A deep link is a special URL that routes to a specific spot, whether that's on a website or in an app. A "mobile deep link" then, is a link that contains all the information needed to take a user directly into an app or a particular location within an app instead of just launching the app's home page. If the app is installed on the user's device - the deep link routes them to the correct location in the app. But what if the app isn't installed? This is where Deferred Deep Linking is used. When the app isn't installed, clicking on the link routes the user to the store to download the app. Deferred Deep linking defer or delay the deep linking process until after the app has been downloaded, and ensures that after they install, the user gets to the right location in the app. diff --git a/doc/Guides.md b/doc/Guides.md index ce5519d6..750124c1 100644 --- a/doc/Guides.md +++ b/doc/Guides.md @@ -69,6 +69,28 @@ Please make sure to go over [this guide](https://support.appsflyer.com/hc/en-us/ --- ## Deep Linking + +> âš ī¸ **IMPORTANT: Flutter 3.27+ Breaking Change** +> +> Starting from Flutter 3.27, the default value for Flutter's deep linking option has changed from `false` to `true`. This means Flutter's built-in deep linking is now enabled by default, which can conflict with third-party deep linking plugins like AppsFlyer. +> +> **If you're using Flutter 3.27 or higher, you MUST disable Flutter's built-in deep linking** by adding the following configurations: +> +> **Android** - Add to your `AndroidManifest.xml` inside the `` tag: +> +> ```xml +> +> ``` +> +> **iOS** - Add to your `Info.plist` file: +> +> ```xml +> FlutterDeepLinkingEnabled +> +> ``` +> +> For more details, see the [official Flutter documentation](https://docs.flutter.dev/release/breaking-changes/deep-links-flag-change). + diff --git a/doc/PurchaseConnector.md b/doc/PurchaseConnector.md index b4ef8f7e..ffecba5a 100644 --- a/doc/PurchaseConnector.md +++ b/doc/PurchaseConnector.md @@ -345,7 +345,7 @@ To test purchases in an iOS environment on a real device with a TestFlight sandb final purchaseConnector = PurchaseConnector( config: PurchaseConnectorConfiguration( sandbox: true, // Enable sandbox for testing - storeKitVersion: StoreKitVersion.storeKit2, // Use StoreKit 2 for enhanced testing + storeKitVersion: StoreKitVersion.storeKit2, logSubscriptions: true, logInApps: true, ), From bdf22a63136499cd51b28d910b6f687ffa8c559a Mon Sep 17 00:00:00 2001 From: Dani Koza <103039399+Dani-Koza-AF@users.noreply.github.com> Date: Tue, 30 Sep 2025 11:04:01 +0300 Subject: [PATCH 31/42] versions bumped + docs updated (#417) --- CHANGELOG.md | 8 ++++++++ README.md | 6 +++--- android/build.gradle | 2 +- .../com/appsflyer/appsflyersdk/AppsFlyerConstants.java | 2 +- doc/PurchaseConnector.md | 9 ++++++++- ios/Classes/AppsflyerSdkPlugin.h | 2 +- ios/appsflyer_sdk.podspec | 6 +++--- lib/src/appsflyer_constants.dart | 2 +- pubspec.yaml | 2 +- 9 files changed, 27 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92557ee2..5f9542a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ # Versions +## 6.17.6 + +- Updated to AppsFlyer SDK v6.17.6 for iOS and Flutter plugin version +- Android AppsFlyer SDK remains at v6.17.3 +- Documents update +- Purchase Connector module updated (to support Google Billing Library 8) + ## 6.17.5 + - Updated to AppsFlyer SDK v6.17.5 for iOS ## 6.17.3 diff --git a/README.md b/README.md index 54b1bc86..05b5997b 100644 --- a/README.md +++ b/README.md @@ -12,12 +12,12 @@ ## SDK Versions - Android AppsFlyer SDK **v6.17.3** -- iOS AppsFlyer SDK **v6.17.5** +- iOS AppsFlyer SDK **v6.17.6** ### Purchase Connector versions -- Android 2.1.1 -- iOS 6.17.5 +- Android 2.2.0 +- iOS 6.17.6 ## ❗❗ Breaking changes when updating to v6.x.x❗❗ diff --git a/android/build.gradle b/android/build.gradle index a5d3aa66..784df06f 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -57,6 +57,6 @@ dependencies { implementation 'com.android.installreferrer:installreferrer:2.2' // implementation 'androidx.core:core-ktx:1.13.1' if (includeConnector) { - implementation 'com.appsflyer:purchase-connector:2.1.1' + implementation 'com.appsflyer:purchase-connector:2.2.0' } } \ No newline at end of file diff --git a/android/src/main/java/com/appsflyer/appsflyersdk/AppsFlyerConstants.java b/android/src/main/java/com/appsflyer/appsflyersdk/AppsFlyerConstants.java index 434d73d3..24673a82 100644 --- a/android/src/main/java/com/appsflyer/appsflyersdk/AppsFlyerConstants.java +++ b/android/src/main/java/com/appsflyer/appsflyersdk/AppsFlyerConstants.java @@ -1,7 +1,7 @@ package com.appsflyer.appsflyersdk; public final class AppsFlyerConstants { - final static String PLUGIN_VERSION = "6.17.5"; + final static String PLUGIN_VERSION = "6.17.6"; final static String AF_APP_INVITE_ONE_LINK = "appInviteOneLink"; final static String AF_HOST_PREFIX = "hostPrefix"; final static String AF_HOST_NAME = "hostName"; diff --git a/doc/PurchaseConnector.md b/doc/PurchaseConnector.md index ffecba5a..6c63c52f 100644 --- a/doc/PurchaseConnector.md +++ b/doc/PurchaseConnector.md @@ -45,9 +45,16 @@ support@appsflyer.com ## âš ī¸ âš ī¸ Important Note âš ī¸ âš ī¸ +> **🚨 BREAKING CHANGE**: Starting with Purchase Connector version 2.2.0, the module now uses **Google Play Billing Library 8.x.x**. While Gradle will automatically resolve to version 8.x.x in your final APK, **we strongly recommend that your app also upgrades to Billing Library 8.x.x or higher** to ensure API compatibility. +> +> **Why this matters:** +> - If your app code still uses **older Billing Library APIs** (e.g., `querySkuDetailsAsync()` from versions 5-7), these APIs were **removed in version 8** and **will cause runtime crashes** (`NoSuchMethodError`). +> - **Version 8 introduced new APIs** like `queryProductDetailsAsync()` that replace the deprecated methods. +> - **Recommendation**: Update your app's billing integration to use Billing Library 8.x.x APIs to prevent runtime issues. + The Purchase Connector feature of the AppsFlyer SDK depends on specific libraries provided by Google and Apple for managing in-app purchases: -- For Android, it depends on the [Google Play Billing Library](https://developer.android.com/google/play/billing/integrate) (Supported versions: 5.x.x - 7.x.x). +- For Android, it depends on the [Google Play Billing Library](https://developer.android.com/google/play/billing/integrate) (Minimum required version: 8.x.x and higher). - For iOS, it depends on [StoreKit](https://developer.apple.com/documentation/storekit). (Supported versions are StoreKit V1 + V2) However, these dependencies aren't actively included with the SDK. This means that the responsibility of managing these dependencies and including the necessary libraries in your project falls on you as the consumer of the SDK. diff --git a/ios/Classes/AppsflyerSdkPlugin.h b/ios/Classes/AppsflyerSdkPlugin.h index 5bc2e992..37fb8674 100644 --- a/ios/Classes/AppsflyerSdkPlugin.h +++ b/ios/Classes/AppsflyerSdkPlugin.h @@ -18,7 +18,7 @@ @end // Appsflyer JS objects -#define kAppsFlyerPluginVersion @"6.17.5" +#define kAppsFlyerPluginVersion @"6.17.6" #define afDevKey @"afDevKey" #define afAppId @"afAppId" #define afIsDebug @"isDebug" diff --git a/ios/appsflyer_sdk.podspec b/ios/appsflyer_sdk.podspec index 64c081e6..5e4ef82d 100644 --- a/ios/appsflyer_sdk.podspec +++ b/ios/appsflyer_sdk.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'appsflyer_sdk' - s.version = '6.17.3' + s.version = '6.17.6' s.summary = 'AppsFlyer Integration for Flutter' s.description = 'AppsFlyer is the market leader in mobile advertising attribution & analytics, helping marketers to pinpoint their targeting, optimize their ad spend and boost their ROI.' s.homepage = 'https://github.com/AppsFlyerSDK/flutter_appsflyer_sdk' @@ -21,12 +21,12 @@ Pod::Spec.new do |s| ss.source_files = 'Classes/**/*' ss.public_header_files = 'Classes/**/*.h' ss.dependency 'Flutter' - ss.ios.dependency 'AppsFlyerFramework','6.17.5' + ss.ios.dependency 'AppsFlyerFramework','6.17.6' end s.subspec 'PurchaseConnector' do |ss| ss.dependency 'Flutter' - ss.ios.dependency 'PurchaseConnector', '6.17.5' + ss.ios.dependency 'PurchaseConnector', '6.17.6' ss.source_files = 'PurchaseConnector/**/*' ss.public_header_files = 'PurchaseConnector/**/*.h' diff --git a/lib/src/appsflyer_constants.dart b/lib/src/appsflyer_constants.dart index e4ea22bf..b27b16f7 100644 --- a/lib/src/appsflyer_constants.dart +++ b/lib/src/appsflyer_constants.dart @@ -3,7 +3,7 @@ part of appsflyer_sdk; enum EmailCryptType { EmailCryptTypeNone, EmailCryptTypeSHA256 } class AppsflyerConstants { - static const String PLUGIN_VERSION = "6.17.3"; + static const String PLUGIN_VERSION = "6.17.6"; static const String AF_DEV_KEY = "afDevKey"; static const String AF_APP_Id = "afAppId"; static const String AF_IS_DEBUG = "isDebug"; diff --git a/pubspec.yaml b/pubspec.yaml index bee08fa0..c9fdc9f2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: appsflyer_sdk description: A Flutter plugin for AppsFlyer SDK. Supports iOS and Android. -version: 6.17.5 +version: 6.17.6 homepage: https://github.com/AppsFlyerSDK/flutter_appsflyer_sdk From c3b40c869d7e0077aa39a2ae7b4eec64084b5b06 Mon Sep 17 00:00:00 2001 From: Dani Koza <103039399+Dani-Koza-AF@users.noreply.github.com> Date: Thu, 9 Oct 2025 14:53:49 +0300 Subject: [PATCH 32/42] Setting up the full CI/CD (#419) --- .github/workflows/ci.yml | 296 ++++++++++++ .github/workflows/production-release.yml | 558 +++++++++++++++++++++++ .github/workflows/rc-release.yml | 503 ++++++++++++++++++++ 3 files changed, 1357 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/production-release.yml create mode 100644 .github/workflows/rc-release.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..fc3724b9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,296 @@ +# ============================================================================= +# CI Workflow - Continuous Integration for Pull Requests and Pushes +# ============================================================================= +# +# Purpose: Validates code quality, runs tests, and builds example apps for +# both Android and iOS platforms on every PR and push to development/master. +# +# What it does: +# 1. Runs Dart/Flutter unit tests +# 2. Builds Android example app (APK) +# 3. Builds iOS example app (simulator + no-codesign IPA) +# 4. Caches dependencies for faster subsequent runs +# +# Triggers: +# - Pull requests to development or master branches +# - Direct pushes to development or master branches +# - Manual workflow dispatch for testing +# +# ============================================================================= + +name: CI - Build & Test + +on: + # Trigger on pull requests targeting main branches + pull_request: + branches: + - development + - master + paths-ignore: + - '**.md' + - 'doc/**' + - 'assets/**' + - '.github/workflows/close_inactive_issues.yml' + - '.github/workflows/responseToSupportIssue*.yml' + + # Trigger on direct pushes to main branches + push: + branches: + - development + - master + paths-ignore: + - '**.md' + - 'doc/**' + - 'assets/**' + + # Allow manual triggering for testing + workflow_dispatch: + + # Allow this workflow to be called by other workflows (reusable workflow) + workflow_call: + +# Ensure only one CI run per PR/branch at a time +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + # =========================================================================== + # Job 1: Run Unit Tests + # =========================================================================== + # Runs all Dart/Flutter unit tests for the plugin + # Uses: Ubuntu runner (fastest and free for public repos) + # =========================================================================== + + test: + name: đŸ§Ē Run Unit Tests + runs-on: ubuntu-latest + + steps: + # Step 1: Checkout the repository code + - name: đŸ“Ĩ Checkout repository + uses: actions/checkout@v4 + + # Step 2: Set up Flutter SDK + # Uses subosito/flutter-action which caches Flutter SDK automatically + - name: 🔧 Setup Flutter SDK + uses: subosito/flutter-action@v2 + with: + flutter-version: 'stable' # Use latest stable Flutter + channel: 'stable' + cache: true # Cache Flutter SDK for faster runs + + # Step 3: Verify Flutter installation + - name: â„šī¸ Display Flutter version + run: | + flutter --version + dart --version + + # Step 4: Get plugin dependencies + # This installs all packages defined in pubspec.yaml + - name: đŸ“Ļ Install plugin dependencies + run: flutter pub get + + # Step 5: Analyze code for issues + # Checks for code quality issues, unused imports, etc. + - name: 🔍 Analyze code + run: flutter analyze + + # Step 6: Format check + # Ensures code follows Dart formatting standards + - name: 💅 Check code formatting + run: dart format --set-exit-if-changed . + + # Step 7: Run unit tests + # Executes all tests in the test/ directory + - name: đŸ§Ē Run unit tests + run: flutter test --coverage + + # Step 8: Upload coverage report (optional) + # Useful for tracking code coverage over time + - name: 📊 Upload coverage to Codecov (optional) + if: success() + uses: codecov/codecov-action@v4 + with: + files: ./coverage/lcov.info + fail_ci_if_error: false # Don't fail CI if coverage upload fails + + # =========================================================================== + # Job 2: Build Android Example App + # =========================================================================== + # Builds the example app for Android to ensure plugin integration works + # Uses: Ubuntu runner with Java 17 + # =========================================================================== + + build-android: + name: 🤖 Build Android Example + runs-on: ubuntu-latest + needs: test # Only run if tests pass + + steps: + # Step 1: Checkout code + - name: đŸ“Ĩ Checkout repository + uses: actions/checkout@v4 + + # Step 2: Set up Java (required for Android builds) + # Android builds require JDK 17 for modern Gradle versions + - name: ☕ Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' # Eclipse Temurin (formerly AdoptOpenJDK) + java-version: '17' + cache: 'gradle' # Cache Gradle dependencies + + # Step 3: Set up Flutter SDK + - name: 🔧 Setup Flutter SDK + uses: subosito/flutter-action@v2 + with: + flutter-version: 'stable' + channel: 'stable' + cache: true + + # Step 4: Display versions for debugging + - name: â„šī¸ Display versions + run: | + flutter --version + java -version + echo "JAVA_HOME: $JAVA_HOME" + + # Step 5: Get plugin dependencies + - name: đŸ“Ļ Install plugin dependencies + run: flutter pub get + + # Step 6: Get example app dependencies + - name: đŸ“Ļ Install example app dependencies + working-directory: example + run: flutter pub get + + # Step 7: Build Android APK (debug mode) + # This validates that the plugin integrates correctly with Android + - name: 🔨 Build Android APK (debug) + working-directory: example + run: flutter build apk --debug + + # Step 8: Build Android App Bundle (release mode, no signing) + # App Bundle is the preferred format for Play Store + - name: 🔨 Build Android App Bundle (release) + working-directory: example + run: flutter build appbundle --release + + # Step 9: Upload build artifacts (optional) + # Useful for manual testing or archiving + - name: 📤 Upload APK artifact + if: success() + uses: actions/upload-artifact@v4 + with: + name: android-apk-debug + path: example/build/app/outputs/flutter-apk/app-debug.apk + retention-days: 7 # Keep for 7 days + + # =========================================================================== + # Job 3: Build iOS Example App + # =========================================================================== + # Builds the example app for iOS to ensure plugin integration works + # Uses: macOS runner (required for Xcode and iOS builds) + # Note: macOS runners consume 10x minutes on private repos + # =========================================================================== + + build-ios: + name: 🍎 Build iOS Example + runs-on: macos-14 # macOS 14 (Sonoma) with Xcode 15+ + needs: test # Only run if tests pass + + steps: + # Step 1: Checkout code + - name: đŸ“Ĩ Checkout repository + uses: actions/checkout@v4 + + # Step 2: Set up Flutter SDK + - name: 🔧 Setup Flutter SDK + uses: subosito/flutter-action@v2 + with: + flutter-version: 'stable' + channel: 'stable' + cache: true + + # Step 3: Display versions + - name: â„šī¸ Display versions + run: | + flutter --version + xcodebuild -version + pod --version + + # Step 4: Get plugin dependencies + - name: đŸ“Ļ Install plugin dependencies + run: flutter pub get + + # Step 5: Get example app dependencies + - name: đŸ“Ļ Install example app dependencies + working-directory: example + run: flutter pub get + + # Step 6: Update CocoaPods repo (ensures latest pod specs) + # This can be slow, so we only update if needed + - name: 🔄 Update CocoaPods repo + working-directory: example/ios + run: pod repo update + + # Step 7: Install CocoaPods dependencies + # This installs native iOS dependencies including AppsFlyer SDK + - name: đŸ“Ļ Install CocoaPods dependencies + working-directory: example/ios + run: pod install + + # Step 8: Build for iOS Simulator (fastest iOS build) + # Validates that the plugin compiles for iOS + - name: 🔨 Build iOS for Simulator + working-directory: example + run: flutter build ios --simulator --debug + + # Step 9: Build iOS IPA without code signing (release mode) + # This validates a full release build without requiring certificates + - name: 🔨 Build iOS IPA (no codesign) + working-directory: example + run: flutter build ipa --release --no-codesign + + # Step 10: Upload build artifacts (optional) + - name: 📤 Upload iOS build artifact + if: success() + uses: actions/upload-artifact@v4 + with: + name: ios-app-unsigned + path: example/build/ios/archive/Runner.xcarchive + retention-days: 7 + + # =========================================================================== + # Job 4: Summary Report + # =========================================================================== + # Provides a summary of all CI jobs + # =========================================================================== + + ci-summary: + name: 📋 CI Summary + runs-on: ubuntu-latest + needs: [test, build-android, build-ios] + if: always() # Run even if previous jobs fail + + steps: + - name: 📊 Check CI Results + run: | + echo "===================================" + echo "CI Pipeline Summary" + echo "===================================" + echo "Test Job: ${{ needs.test.result }}" + echo "Android Build: ${{ needs.build-android.result }}" + echo "iOS Build: ${{ needs.build-ios.result }}" + echo "===================================" + + # Fail this job if any required job failed + if [[ "${{ needs.test.result }}" != "success" ]] || \ + [[ "${{ needs.build-android.result }}" != "success" ]] || \ + [[ "${{ needs.build-ios.result }}" != "success" ]]; then + echo "❌ CI Pipeline Failed" + exit 1 + fi + + echo "✅ CI Pipeline Passed Successfully" diff --git a/.github/workflows/production-release.yml b/.github/workflows/production-release.yml new file mode 100644 index 00000000..50a87c65 --- /dev/null +++ b/.github/workflows/production-release.yml @@ -0,0 +1,558 @@ +# ============================================================================= +# Production Release Workflow - Publish to pub.dev +# ============================================================================= +# +# Purpose: Publishes the Flutter plugin to pub.dev after successful QA testing. +# This workflow is triggered when a release PR is merged to master. +# +# What it does: +# 1. Validates the merge is from a release branch +# 2. Runs full CI pipeline one final time +# 3. Publishes the plugin to pub.dev +# 4. Creates a GitHub release with release notes +# 5. Notifies team via Slack (placeholder for future) +# +# Prerequisites: +# - Release branch has been tested and approved +# - PR from release branch to master has been created and approved +# - All CI checks have passed +# +# Triggers: +# - Pull request closed (merged) to master branch from releases/** branches +# - Manual workflow dispatch (for republishing or testing) +# +# ============================================================================= + +name: Production Release - Publish to pub.dev + +on: + # Trigger when PR to master is merged + pull_request: + types: + - closed + branches: + - master + + # Allow manual triggering + workflow_dispatch: + inputs: + version: + description: 'Version to release (must match pubspec.yaml)' + required: true + type: string + skip_tests: + description: 'Skip CI tests (use with caution!)' + required: false + type: boolean + default: false + dry_run: + description: 'Dry run (do not actually publish)' + required: false + type: boolean + default: false + +# Ensure only one production release runs at a time +concurrency: + group: production-release + cancel-in-progress: false + +jobs: + # =========================================================================== + # Job 1: Validate Release + # =========================================================================== + # Ensures this is a valid release merge from a release branch + # =========================================================================== + + validate-release: + name: 🔍 Validate Release + runs-on: ubuntu-latest + + # Only run if PR was actually merged (not just closed) + if: github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true + + outputs: + version: ${{ steps.get-version.outputs.version }} + is_valid: ${{ steps.validate.outputs.is_valid }} + is_release_branch: ${{ steps.validate.outputs.is_release_branch }} + + steps: + - name: đŸ“Ĩ Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: 🔍 Validate release source + id: validate + run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + echo "Manual workflow dispatch - skipping branch validation" + echo "is_release_branch=true" >> $GITHUB_OUTPUT + echo "is_valid=true" >> $GITHUB_OUTPUT + else + # Check if the merged PR came from a release branch + SOURCE_BRANCH="${{ github.event.pull_request.head.ref }}" + echo "Source branch: $SOURCE_BRANCH" + + if [[ $SOURCE_BRANCH =~ ^releases/ ]]; then + echo "✅ Valid release branch: $SOURCE_BRANCH" + echo "is_release_branch=true" >> $GITHUB_OUTPUT + echo "is_valid=true" >> $GITHUB_OUTPUT + else + echo "âš ī¸ Not a release branch: $SOURCE_BRANCH" + echo "Production release should only be triggered from release branches" + echo "is_release_branch=false" >> $GITHUB_OUTPUT + echo "is_valid=false" >> $GITHUB_OUTPUT + exit 1 + fi + fi + + - name: 📝 Get version from pubspec.yaml + id: get-version + run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + VERSION="${{ github.event.inputs.version }}" + echo "Using manual version: $VERSION" + else + # Extract version from pubspec.yaml + VERSION=$(grep "^version:" pubspec.yaml | sed 's/version: //' | tr -d ' ') + echo "Extracted version from pubspec.yaml: $VERSION" + fi + + # Validate version format (X.Y.Z) + if [[ $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "✅ Valid version format: $VERSION" + echo "version=$VERSION" >> $GITHUB_OUTPUT + else + echo "❌ Invalid version format: $VERSION" + echo "Expected format: X.Y.Z (e.g., 6.17.6)" + exit 1 + fi + + - name: đŸˇī¸ Check if tag already exists + run: | + VERSION="${{ steps.get-version.outputs.version }}" + + # Check if tag exists locally or remotely + if git rev-parse "$VERSION" >/dev/null 2>&1; then + echo "âš ī¸ Tag $VERSION already exists locally" + + if [[ "${{ github.event.inputs.dry_run }}" == "true" ]]; then + echo "Dry run mode - continuing anyway" + else + echo "❌ Cannot create duplicate release" + exit 1 + fi + fi + + # Check remote tags + git fetch --tags + if git rev-parse "origin/$VERSION" >/dev/null 2>&1; then + echo "âš ī¸ Tag $VERSION already exists remotely" + + if [[ "${{ github.event.inputs.dry_run }}" == "true" ]]; then + echo "Dry run mode - continuing anyway" + else + echo "❌ Cannot create duplicate release" + exit 1 + fi + fi + + echo "✅ Tag $VERSION does not exist - safe to proceed" + + # =========================================================================== + # Job 2: Run Final CI Check + # =========================================================================== + # Runs the full CI pipeline one last time before publishing + # =========================================================================== + + final-ci-check: + name: 🚀 Final CI Check + needs: validate-release + if: needs.validate-release.outputs.is_valid == 'true' && github.event.inputs.skip_tests != 'true' + uses: ./.github/workflows/ci.yml + secrets: inherit + + # =========================================================================== + # Job 3: Publish to pub.dev + # =========================================================================== + # Publishes the Flutter plugin to pub.dev + # =========================================================================== + + publish-to-pubdev: + name: đŸ“Ļ Publish to pub.dev + runs-on: ubuntu-latest + needs: [validate-release, final-ci-check] + if: always() && needs.validate-release.outputs.is_valid == 'true' + + steps: + - name: đŸ“Ĩ Checkout repository + uses: actions/checkout@v4 + + - name: 🔧 Setup Flutter SDK + uses: subosito/flutter-action@v2 + with: + flutter-version: 'stable' + channel: 'stable' + cache: true + + - name: â„šī¸ Display Flutter version + run: | + flutter --version + dart --version + + - name: đŸ“Ļ Get dependencies + run: flutter pub get + + - name: 🔍 Validate package + run: | + echo "Running pub publish dry-run to validate package..." + flutter pub publish --dry-run + + - name: 📝 Check pub.dev credentials + run: | + # Check if pub-credentials.json exists + # Note: This should be set up as a repository secret + if [[ -z "${{ secrets.PUB_DEV_CREDENTIALS }}" ]]; then + echo "âš ī¸ PUB_DEV_CREDENTIALS secret not found" + echo "Please set up pub.dev credentials as a repository secret" + echo "See: https://dart.dev/tools/pub/automated-publishing" + + if [[ "${{ github.event.inputs.dry_run }}" != "true" ]]; then + exit 1 + fi + else + echo "✅ pub.dev credentials found" + fi + + - name: 🚀 Publish to pub.dev + if: github.event.inputs.dry_run != 'true' + run: | + VERSION="${{ needs.validate-release.outputs.version }}" + echo "Publishing version $VERSION to pub.dev..." + + # Set up credentials + mkdir -p ~/.config/dart + echo '${{ secrets.PUB_DEV_CREDENTIALS }}' > ~/.config/dart/pub-credentials.json + + # Publish to pub.dev (non-interactive) + flutter pub publish --force + + # Clean up credentials + rm ~/.config/dart/pub-credentials.json + + echo "✅ Successfully published to pub.dev" + + - name: đŸˇī¸ Verify publication + if: github.event.inputs.dry_run != 'true' + run: | + VERSION="${{ needs.validate-release.outputs.version }}" + + echo "Waiting 30 seconds for pub.dev to index the package..." + sleep 30 + + # Check if the version is available on pub.dev + PACKAGE_INFO=$(curl -s "https://pub.dev/api/packages/appsflyer_sdk") + + if echo "$PACKAGE_INFO" | grep -q "\"version\":\"$VERSION\""; then + echo "✅ Version $VERSION is now available on pub.dev" + else + echo "âš ī¸ Version $VERSION not yet visible on pub.dev" + echo "This is normal - it may take a few minutes to appear" + fi + + # =========================================================================== + # Job 4: Create GitHub Release + # =========================================================================== + # Creates an official GitHub release with release notes + # =========================================================================== + + create-github-release: + name: đŸˇī¸ Create GitHub Release + runs-on: ubuntu-latest + needs: [validate-release, publish-to-pubdev] + if: always() && needs.validate-release.outputs.is_valid == 'true' && github.event.inputs.dry_run != 'true' + + steps: + - name: đŸ“Ĩ Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: 📝 Extract release notes from CHANGELOG + id: changelog + run: | + VERSION="${{ needs.validate-release.outputs.version }}" + + echo "Extracting release notes for version $VERSION from CHANGELOG.md" + + # Try to extract the section for this version from CHANGELOG.md + if [ -f "CHANGELOG.md" ]; then + # Extract content between ## VERSION and the next ## heading + RELEASE_NOTES=$(awk "/## $VERSION/,/^## [0-9]/" CHANGELOG.md | sed '1d;$d') + + if [ -z "$RELEASE_NOTES" ]; then + echo "âš ī¸ Could not find release notes for version $VERSION in CHANGELOG.md" + RELEASE_NOTES="Release version $VERSION. See [CHANGELOG.md](CHANGELOG.md) for details." + fi + else + echo "âš ī¸ CHANGELOG.md not found" + RELEASE_NOTES="Release version $VERSION." + fi + + # Save to file for use in release + echo "$RELEASE_NOTES" > release_notes.md + + echo "Release notes extracted" + + - name: 📝 Enhance release notes + run: | + VERSION="${{ needs.validate-release.outputs.version }}" + + # Get SDK versions from files + ANDROID_SDK_VERSION=$(grep "implementation 'com.appsflyer:af-android-sdk:" android/build.gradle | sed -n "s/.*af-android-sdk:\([^']*\).*/\1/p" | head -1) + IOS_SDK_VERSION=$(grep "s.ios.dependency 'AppsFlyerFramework'" ios/appsflyer_sdk.podspec | sed -n "s/.*AppsFlyerFramework',.*'\([^']*\)'.*/\1/p" | head -1) + + # Create enhanced release notes + cat > final_release_notes.md << EOF + # AppsFlyer Flutter Plugin v$VERSION + + ## đŸ“Ļ Installation + + Add to your \`pubspec.yaml\`: + + \`\`\`yaml + dependencies: + appsflyer_sdk: ^$VERSION + \`\`\` + + Then run: + \`\`\`bash + flutter pub get + \`\`\` + + ## 📋 Changes in This Release + + $(cat release_notes.md) + + ## 🔧 SDK Versions + + - **Android**: AppsFlyer SDK v$ANDROID_SDK_VERSION + - **iOS**: AppsFlyer SDK v$IOS_SDK_VERSION + + ## 📚 Documentation + + - [Installation Guide](https://github.com/${{ github.repository }}/blob/master/doc/Installation.md) + - [Basic Integration](https://github.com/${{ github.repository }}/blob/master/doc/BasicIntegration.md) + - [API Documentation](https://github.com/${{ github.repository }}/blob/master/doc/API.md) + - [Sample App](https://github.com/${{ github.repository }}/tree/master/example) + + ## 🔗 Links + + - [pub.dev Package](https://pub.dev/packages/appsflyer_sdk) + - [API Reference](https://pub.dev/documentation/appsflyer_sdk/latest/) + - [GitHub Repository](https://github.com/${{ github.repository }}) + - [AppsFlyer Developer Hub](https://dev.appsflyer.com/) + + ## đŸ’Ŧ Support + + For issues and questions, please contact + EOF + + echo "Enhanced release notes created" + + - name: đŸˇī¸ Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ needs.validate-release.outputs.version }} + name: v${{ needs.validate-release.outputs.version }} + body_path: final_release_notes.md + draft: false + prerelease: false + generate_release_notes: false + token: ${{ secrets.GITHUB_TOKEN }} + + - name: ✅ Release created + run: | + VERSION="${{ needs.validate-release.outputs.version }}" + echo "✅ GitHub release v$VERSION created successfully" + echo "🔗 https://github.com/${{ github.repository }}/releases/tag/$VERSION" + + # =========================================================================== + # Job 5: Notify Team (Placeholder for Slack Integration) + # =========================================================================== + # Sends notification to Slack channel about the production release + # =========================================================================== + + notify-team: + name: đŸ“ĸ Notify Team + runs-on: ubuntu-latest + needs: [validate-release, publish-to-pubdev, create-github-release] + if: always() && github.event.inputs.dry_run != 'true' + + steps: + - name: đŸ“Ĩ Checkout repository + uses: actions/checkout@v4 + + - name: 📝 Extract SDK versions and changelog + id: extract-info + run: | + VERSION="${{ needs.validate-release.outputs.version }}" + + # Extract Android SDK version + ANDROID_SDK_VERSION=$(grep "implementation 'com.appsflyer:af-android-sdk:" android/build.gradle | sed -n "s/.*af-android-sdk:\([^']*\).*/\1/p" | head -1) + echo "android_sdk=$ANDROID_SDK_VERSION" >> $GITHUB_OUTPUT + + # Extract iOS SDK version from podspec + IOS_SDK_VERSION=$(grep "s.ios.dependency 'AppsFlyerFramework'" ios/appsflyer_sdk.podspec | sed -n "s/.*AppsFlyerFramework',.*'\([^']*\)'.*/\1/p" | head -1) + echo "ios_sdk=$IOS_SDK_VERSION" >> $GITHUB_OUTPUT + + # Extract Purchase Connector versions from README (fallback to default if not found) + ANDROID_PC_VERSION=$(grep -A 1 "### Purchase Connector versions" README.md | grep "Android" | sed -n 's/.*Android \([0-9.]*\).*/\1/p' || echo "N/A") + IOS_PC_VERSION=$(grep -A 2 "### Purchase Connector versions" README.md | grep "iOS" | sed -n 's/.*iOS \([0-9.]*\).*/\1/p' || echo "N/A") + echo "android_pc=$ANDROID_PC_VERSION" >> $GITHUB_OUTPUT + echo "ios_pc=$IOS_PC_VERSION" >> $GITHUB_OUTPUT + + # Extract changelog for this version + if [ -f "CHANGELOG.md" ]; then + # Extract bullet points for this version + CHANGELOG=$(awk "/## $VERSION/,/^## [0-9]/" CHANGELOG.md | grep "^-" | sed 's/^- /â€ĸ /' | head -5) + if [ -z "$CHANGELOG" ]; then + CHANGELOG="â€ĸ Check CHANGELOG.md for details" + fi + else + CHANGELOG="â€ĸ Check release notes for details" + fi + + # Save to file and encode for JSON + echo "$CHANGELOG" > /tmp/changelog.txt + echo "changelog<> $GITHUB_OUTPUT + echo "$CHANGELOG" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: đŸŽĢ Fetch Jira tickets + id: jira-tickets + run: | + VERSION="${{ needs.validate-release.outputs.version }}" + JIRA_FIX_VERSION="Flutter SDK v$VERSION" + + # Check if Jira credentials are available + if [[ -z "${{ secrets.CI_JIRA_EMAIL }}" ]] || [[ -z "${{ secrets.CI_JIRA_TOKEN }}" ]]; then + echo "âš ī¸ Jira credentials not configured" + echo "tickets=No linked tickets (Jira not configured)" >> $GITHUB_OUTPUT + exit 0 + fi + + # Fetch tickets from Jira with this fix version + JIRA_DOMAIN="${{ secrets.CI_JIRA_DOMAIN || 'appsflyer.atlassian.net' }}" + + # Create auth header + AUTH=$(echo -n "${{ secrets.CI_JIRA_EMAIL }}:${{ secrets.CI_JIRA_TOKEN }}" | base64) + + # Query Jira API + RESPONSE=$(curl -s -X GET \ + -H "Authorization: Basic $AUTH" \ + -H "Content-Type: application/json" \ + "https://${JIRA_DOMAIN}/rest/api/3/search?jql=fixVersion=\"${JIRA_FIX_VERSION}\"&fields=key,summary&maxResults=20") + + # Extract ticket keys and create links + TICKETS=$(echo "$RESPONSE" | jq -r '.issues[]? | "https://'"${JIRA_DOMAIN}"'/browse/\(.key)"' | head -10) + + if [ -z "$TICKETS" ]; then + echo "tickets=No linked tickets found for version: $JIRA_FIX_VERSION" >> $GITHUB_OUTPUT + else + # Format tickets as newline-separated list + echo "tickets<> $GITHUB_OUTPUT + echo "$TICKETS" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + fi + + - name: đŸ“ĸ Determine status + id: status + run: | + PUBLISH_STATUS="${{ needs.publish-to-pubdev.result }}" + RELEASE_STATUS="${{ needs.create-github-release.result }}" + + if [[ "$PUBLISH_STATUS" == "success" ]] && [[ "$RELEASE_STATUS" == "success" ]]; then + echo "success=true" >> $GITHUB_OUTPUT + else + echo "success=false" >> $GITHUB_OUTPUT + fi + + - name: 📨 Send Slack notification + if: steps.status.outputs.success == 'true' + uses: slackapi/slack-github-action@v1 + with: + payload: | + { + "text": "\n:flutter::flutter::flutter::flutter::flutter::flutter::flutter::flutter::flutter::flutter::flutter::flutter:\n\n*Flutter:*\nappsflyer_sdk: ^${{ needs.validate-release.outputs.version }} is published to Production.\n\n*Sources:*\n:github: https://github.com/${{ github.repository }}/tree/master\n:pubdev: https://pub.dev/packages/appsflyer_sdk/versions/${{ needs.validate-release.outputs.version }}\n\n*Changes and fixes:*\n${{ steps.extract-info.outputs.changelog }}\n\n*Tests:*\n:white_check_mark: CI pipeline passed.\n:white_check_mark: Unit tests passed.\n:white_check_mark: Android and iOS builds successful.\n\n*Linked tickets and issues:*\n${{ steps.jira-tickets.outputs.tickets }}\n\n*Native SDK's:*\n:android: ${{ steps.extract-info.outputs.android_sdk }}\n:apple: ${{ steps.extract-info.outputs.ios_sdk }}\n\n*Purchase Connector:*\n:android: ${{ steps.extract-info.outputs.android_pc }}\n:apple: ${{ steps.extract-info.outputs.ios_pc }}\n\n:flutter::flutter::flutter::flutter::flutter::flutter::flutter::flutter::flutter::flutter::flutter::flutter:" + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_WEBHOOK_URL }} + + - name: 📨 Send failure notification + if: steps.status.outputs.success == 'false' + uses: slackapi/slack-github-action@v1 + with: + payload: | + { + "text": "\n:warning: *Flutter Plugin Release Failed*\n\nVersion: ${{ needs.validate-release.outputs.version }}\nStatus: Release encountered issues\n\nPlease check the workflow logs: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_WEBHOOK_URL }} + + # =========================================================================== + # Job 6: Production Release Summary + # =========================================================================== + # Provides a comprehensive summary of the production release + # =========================================================================== + + release-summary: + name: 📋 Release Summary + runs-on: ubuntu-latest + needs: [validate-release, final-ci-check, publish-to-pubdev, create-github-release] + if: always() + + steps: + - name: 📊 Display Release Summary + run: | + echo "=========================================" + echo "Production Release Summary" + echo "=========================================" + echo "Version: ${{ needs.validate-release.outputs.version }}" + echo "Dry Run: ${{ github.event.inputs.dry_run }}" + echo "-----------------------------------------" + echo "Validation: ${{ needs.validate-release.result }}" + echo "Final CI Check: ${{ needs.final-ci-check.result }}" + echo "pub.dev Publish: ${{ needs.publish-to-pubdev.result }}" + echo "GitHub Release: ${{ needs.create-github-release.result }}" + echo "=========================================" + + if [[ "${{ github.event.inputs.dry_run }}" == "true" ]]; then + echo "â„šī¸ This was a DRY RUN - no actual publishing occurred" + exit 0 + fi + + # Check if all critical jobs succeeded + if [[ "${{ needs.validate-release.result }}" == "success" ]] && \ + [[ "${{ needs.publish-to-pubdev.result }}" == "success" ]] && \ + [[ "${{ needs.create-github-release.result }}" == "success" ]]; then + echo "" + echo "✅ Production Release Completed Successfully!" + echo "" + echo "🎉 Version ${{ needs.validate-release.outputs.version }} is now live!" + echo "" + echo "đŸ“Ļ pub.dev: https://pub.dev/packages/appsflyer_sdk" + echo "đŸˇī¸ GitHub: https://github.com/${{ github.repository }}/releases/tag/${{ needs.validate-release.outputs.version }}" + echo "" + echo "Next Steps:" + echo "1. Verify the package on pub.dev" + echo "2. Update documentation if needed" + echo "3. Announce the release to the community" + echo "4. Monitor for any issues or feedback" + else + echo "" + echo "❌ Production Release Failed" + echo "" + echo "Please check the logs above for details and retry if necessary" + exit 1 + fi diff --git a/.github/workflows/rc-release.yml b/.github/workflows/rc-release.yml new file mode 100644 index 00000000..628463d4 --- /dev/null +++ b/.github/workflows/rc-release.yml @@ -0,0 +1,503 @@ +# ============================================================================= +# RC (Release Candidate) Workflow - Pre-Production Release +# ============================================================================= +# +# Purpose: Creates a release candidate for QA testing before production release. +# This workflow is triggered when a release branch is created or manually. +# +# What it does: +# 1. Validates the release branch naming convention +# 2. Runs full CI pipeline (tests + builds) +# 3. Updates version numbers in relevant files +# 4. Creates a pre-release tag on GitHub +# 5. Optionally notifies team via Slack (placeholder for future) +# +# Branch Convention: releases/X.x.x/X.Y.x/X.Y.Z-rcN +# Example: releases/6.x.x/6.18.x/6.18.0-rc1 +# Where: X, Y, Z are version numbers, and lowercase 'x' is literal +# +# Triggers: +# - Push to branches matching releases/** pattern +# - Manual workflow dispatch with version input +# +# ============================================================================= + +name: RC - Release Candidate + +on: + # Trigger on push to release branches + push: + branches: + - 'releases/**' + + # Allow manual triggering with version specification + workflow_dispatch: + inputs: + version: + description: 'Release version (e.g., 6.18.0-rc1)' + required: true + type: string + skip_tests: + description: 'Skip tests and builds (for testing workflow only)' + required: false + type: boolean + default: false + +# Prevent multiple RC workflows from running simultaneously +concurrency: + group: rc-release-${{ github.ref }} + cancel-in-progress: false # Don't cancel, let it finish + +jobs: + # =========================================================================== + # Job 1: Validate Release Branch + # =========================================================================== + # Ensures the branch follows naming conventions and extracts version info + # =========================================================================== + + validate-release: + name: 🔍 Validate Release Branch + runs-on: ubuntu-latest + + outputs: + version: ${{ steps.extract-version.outputs.version }} + is_rc: ${{ steps.extract-version.outputs.is_rc }} + is_valid: ${{ steps.extract-version.outputs.is_valid }} + + steps: + - name: đŸ“Ĩ Checkout repository + uses: actions/checkout@v4 + + - name: 🔍 Extract and validate version + id: extract-version + run: | + # Determine version from branch name or manual input + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + VERSION="${{ github.event.inputs.version }}" + echo "Using manual version: $VERSION" + else + # Extract version from branch name: releases/X.x.x/X.Y.x/X.Y.Z-rcN + # Example: releases/6.x.x/6.18.x/6.18.0-rc1 + BRANCH_NAME="${{ github.ref_name }}" + echo "Branch name: $BRANCH_NAME" + + # Extract version using regex + # Pattern: releases/X.x.x/X.Y.x/X.Y.Z-rcN where x is literal 'x' + if [[ $BRANCH_NAME =~ releases/([0-9]+)\.x\.x/([0-9]+\.[0-9]+)\.x/([0-9]+\.[0-9]+\.[0-9]+-rc[0-9]+)$ ]]; then + VERSION="${BASH_REMATCH[3]}" + MAJOR="${BASH_REMATCH[1]}" + MAJOR_MINOR="${BASH_REMATCH[2]}" + + echo "Extracted version: $VERSION" + echo "Major version: $MAJOR" + echo "Major.Minor: $MAJOR_MINOR" + + # Validate that version starts with the correct major.minor + VERSION_PREFIX=$(echo "$VERSION" | grep -oE '^[0-9]+\.[0-9]+') + if [[ "$VERSION_PREFIX" != "$MAJOR_MINOR" ]]; then + echo "❌ Version mismatch!" + echo "Expected version to start with: $MAJOR_MINOR" + echo "Got: $VERSION" + exit 1 + fi + + # Validate that major version matches + VERSION_MAJOR=$(echo "$VERSION" | grep -oE '^[0-9]+') + if [[ "$VERSION_MAJOR" != "$MAJOR" ]]; then + echo "❌ Major version mismatch!" + echo "Expected major version: $MAJOR" + echo "Got: $VERSION_MAJOR" + exit 1 + fi + else + echo "❌ Invalid branch name format!" + echo "Expected: releases/X.x.x/X.Y.x/X.Y.Z-rcN" + echo "Example: releases/6.x.x/6.18.x/6.18.0-rc1" + echo "Got: $BRANCH_NAME" + exit 1 + fi + fi + + # Validate version format + if [[ $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+(-rc[0-9]+)?$ ]]; then + echo "✅ Valid version format: $VERSION" + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "is_valid=true" >> $GITHUB_OUTPUT + + # Check if it's an RC version + if [[ $VERSION =~ -rc[0-9]+$ ]]; then + echo "is_rc=true" >> $GITHUB_OUTPUT + echo "đŸ“Ļ This is a Release Candidate" + else + echo "is_rc=false" >> $GITHUB_OUTPUT + echo "đŸ“Ļ This is a production version" + fi + else + echo "❌ Invalid version format: $VERSION" + echo "is_valid=false" >> $GITHUB_OUTPUT + exit 1 + fi + + # =========================================================================== + # Job 2: Run CI Pipeline + # =========================================================================== + # Reuses the main CI workflow to run tests and builds + # =========================================================================== + + run-ci: + name: 🚀 Run CI Pipeline + needs: validate-release + if: ${{ needs.validate-release.outputs.is_valid == 'true' && github.event.inputs.skip_tests != 'true' }} + uses: ./.github/workflows/ci.yml + secrets: inherit + + # =========================================================================== + # Job 3: Update Version Files + # =========================================================================== + # Updates pubspec.yaml and other version-related files + # =========================================================================== + + update-version: + name: 📝 Update Version Files + runs-on: ubuntu-latest + needs: [validate-release, run-ci] + if: always() && needs.validate-release.outputs.is_valid == 'true' + + steps: + - name: đŸ“Ĩ Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 # Fetch all history for proper tagging + + - name: 🔧 Setup Flutter SDK + uses: subosito/flutter-action@v2 + with: + flutter-version: 'stable' + channel: 'stable' + cache: true + + - name: 📝 Update pubspec.yaml version + run: | + VERSION="${{ needs.validate-release.outputs.version }}" + + # Remove -rcN suffix for pubspec.yaml (pub.dev doesn't support pre-release tags) + PUBSPEC_VERSION=$(echo $VERSION | sed 's/-rc[0-9]*$//') + + echo "Updating pubspec.yaml to version: $PUBSPEC_VERSION" + + # Update version in pubspec.yaml + sed -i.bak "s/^version: .*/version: $PUBSPEC_VERSION/" pubspec.yaml + rm pubspec.yaml.bak + + # Verify the change + echo "Updated pubspec.yaml:" + grep "^version:" pubspec.yaml + + - name: 📝 Update Android plugin version constant + run: | + VERSION="${{ needs.validate-release.outputs.version }}" + + # Find and update kPluginVersion in Android constants + ANDROID_CONSTANTS_FILE="android/src/main/java/com/appsflyer/appsflyersdk/AppsflyerConstants.java" + + if [ -f "$ANDROID_CONSTANTS_FILE" ]; then + echo "Updating Android plugin version to: $VERSION" + sed -i.bak "s/kPluginVersion = \".*\"/kPluginVersion = \"$VERSION\"/" "$ANDROID_CONSTANTS_FILE" + rm "${ANDROID_CONSTANTS_FILE}.bak" + + echo "Updated Android constants:" + grep "kPluginVersion" "$ANDROID_CONSTANTS_FILE" || echo "Pattern not found" + else + echo "âš ī¸ Android constants file not found, skipping" + fi + + - name: 📝 Update iOS plugin version constant + run: | + VERSION="${{ needs.validate-release.outputs.version }}" + + # Find and update kPluginVersion in iOS constants + IOS_PLUGIN_FILE="ios/Classes/AppsflyerSdkPlugin.m" + + if [ -f "$IOS_PLUGIN_FILE" ]; then + echo "Updating iOS plugin version to: $VERSION" + sed -i.bak "s/kPluginVersion = @\".*\"/kPluginVersion = @\"$VERSION\"/" "$IOS_PLUGIN_FILE" + rm "${IOS_PLUGIN_FILE}.bak" + + echo "Updated iOS plugin file:" + grep "kPluginVersion" "$IOS_PLUGIN_FILE" || echo "Pattern not found" + else + echo "âš ī¸ iOS plugin file not found, skipping" + fi + + - name: 📝 Update podspec version + run: | + VERSION="${{ needs.validate-release.outputs.version }}" + PODSPEC_VERSION=$(echo $VERSION | sed 's/-rc[0-9]*$//') + + PODSPEC_FILE="ios/appsflyer_sdk.podspec" + + if [ -f "$PODSPEC_FILE" ]; then + echo "Updating podspec to version: $PODSPEC_VERSION" + sed -i.bak "s/s\.version.*=.*/s.version = '$PODSPEC_VERSION'/" "$PODSPEC_FILE" + rm "${PODSPEC_FILE}.bak" + + echo "Updated podspec:" + grep "s.version" "$PODSPEC_FILE" + else + echo "âš ī¸ Podspec file not found, skipping" + fi + + - name: 💾 Commit version changes + run: | + VERSION="${{ needs.validate-release.outputs.version }}" + + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + + # Check if there are changes to commit + if [[ -n $(git status -s) ]]; then + git add pubspec.yaml android/ ios/ + git commit -m "chore: bump version to $VERSION" + git push + echo "✅ Version changes committed and pushed" + else + echo "â„šī¸ No version changes to commit" + fi + + # =========================================================================== + # Job 4: Create Pre-Release + # =========================================================================== + # Creates a GitHub pre-release with the RC tag + # =========================================================================== + + create-prerelease: + name: đŸˇī¸ Create Pre-Release + runs-on: ubuntu-latest + needs: [validate-release, run-ci, update-version] + if: always() && needs.validate-release.outputs.is_rc == 'true' + + steps: + - name: đŸ“Ĩ Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.ref }} + + - name: 📝 Generate release notes + id: release-notes + run: | + VERSION="${{ needs.validate-release.outputs.version }}" + + # Extract changes from CHANGELOG.md for this version + echo "Extracting release notes for version $VERSION" + + # Create release notes + cat > release_notes.md << EOF + # AppsFlyer Flutter Plugin - Release Candidate $VERSION + + ## 🚀 Release Candidate for Testing + + This is a pre-release version for QA testing. Please do not use in production. + + ## 📋 Changes + + Please refer to [CHANGELOG.md](https://github.com/${{ github.repository }}/blob/${{ github.ref_name }}/CHANGELOG.md) for detailed changes. + + ## đŸ§Ē Testing Instructions + + 1. Add to your \`pubspec.yaml\`: + \`\`\`yaml + dependencies: + appsflyer_sdk: + git: + url: https://github.com/${{ github.repository }}.git + ref: $VERSION + \`\`\` + + 2. Run \`flutter pub get\` + 3. Test the integration thoroughly + 4. Report any issues to the development team + + ## đŸ“Ļ SDK Versions + + - Android AppsFlyer SDK: (check android/build.gradle) + - iOS AppsFlyer SDK: (check ios/appsflyer_sdk.podspec) + + --- + + **Note**: This is a pre-release and should not be used in production applications. + EOF + + echo "Release notes generated" + + - name: đŸˇī¸ Create GitHub Pre-Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ needs.validate-release.outputs.version }} + name: Release Candidate ${{ needs.validate-release.outputs.version }} + body_path: release_notes.md + draft: false + prerelease: true # Mark as pre-release + generate_release_notes: false + token: ${{ secrets.GITHUB_TOKEN }} + + # =========================================================================== + # Job 5: Notify Team (Placeholder for Slack Integration) + # =========================================================================== + # Sends notification to Slack channel about the RC release + # =========================================================================== + + notify-team: + name: đŸ“ĸ Notify Team + runs-on: ubuntu-latest + needs: [validate-release, create-prerelease] + if: always() + + steps: + - name: đŸ“Ĩ Checkout repository + uses: actions/checkout@v4 + + - name: 📝 Extract SDK versions and changelog + id: extract-info + run: | + VERSION="${{ needs.validate-release.outputs.version }}" + BASE_VERSION=$(echo "$VERSION" | sed 's/-rc[0-9]*$//') + + # Extract Android SDK version + ANDROID_SDK_VERSION=$(grep "implementation 'com.appsflyer:af-android-sdk:" android/build.gradle | sed -n "s/.*af-android-sdk:\([^']*\).*/\1/p" | head -1) + echo "android_sdk=$ANDROID_SDK_VERSION" >> $GITHUB_OUTPUT + + # Extract iOS SDK version + IOS_SDK_VERSION=$(grep "s.ios.dependency 'AppsFlyerFramework'" ios/appsflyer_sdk.podspec | sed -n "s/.*AppsFlyerFramework',.*'\([^']*\)'.*/\1/p" | head -1) + echo "ios_sdk=$IOS_SDK_VERSION" >> $GITHUB_OUTPUT + + # Extract Purchase Connector versions + ANDROID_PC_VERSION=$(grep -A 1 "### Purchase Connector versions" README.md | grep "Android" | sed -n 's/.*Android \([0-9.]*\).*/\1/p' || echo "N/A") + IOS_PC_VERSION=$(grep -A 2 "### Purchase Connector versions" README.md | grep "iOS" | sed -n 's/.*iOS \([0-9.]*\).*/\1/p' || echo "N/A") + echo "android_pc=$ANDROID_PC_VERSION" >> $GITHUB_OUTPUT + echo "ios_pc=$IOS_PC_VERSION" >> $GITHUB_OUTPUT + + # Extract changelog for this version (use base version without -rc suffix) + if [ -f "CHANGELOG.md" ]; then + CHANGELOG=$(awk "/## $BASE_VERSION/,/^## [0-9]/" CHANGELOG.md | grep "^-" | sed 's/^- /â€ĸ /' | head -5) + if [ -z "$CHANGELOG" ]; then + CHANGELOG="â€ĸ Check CHANGELOG.md for details" + fi + else + CHANGELOG="â€ĸ Check release notes for details" + fi + + echo "changelog<> $GITHUB_OUTPUT + echo "$CHANGELOG" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: đŸŽĢ Fetch Jira tickets + id: jira-tickets + run: | + VERSION="${{ needs.validate-release.outputs.version }}" + BASE_VERSION=$(echo "$VERSION" | sed 's/-rc[0-9]*$//') + JIRA_FIX_VERSION="Flutter SDK v$BASE_VERSION" + + # Check if Jira credentials are available + if [[ -z "${{ secrets.CI_JIRA_EMAIL }}" ]] || [[ -z "${{ secrets.CI_JIRA_TOKEN }}" ]]; then + echo "âš ī¸ Jira credentials not configured" + echo "tickets=No linked tickets (Jira not configured)" >> $GITHUB_OUTPUT + exit 0 + fi + + JIRA_DOMAIN="${{ secrets.CI_JIRA_DOMAIN || 'appsflyer.atlassian.net' }}" + AUTH=$(echo -n "${{ secrets.CI_JIRA_EMAIL }}:${{ secrets.CI_JIRA_TOKEN }}" | base64) + + # Query Jira API + RESPONSE=$(curl -s -X GET \ + -H "Authorization: Basic $AUTH" \ + -H "Content-Type: application/json" \ + "https://${JIRA_DOMAIN}/rest/api/3/search?jql=fixVersion=\"${JIRA_FIX_VERSION}\"&fields=key,summary&maxResults=20") + + # Extract ticket keys and create links + TICKETS=$(echo "$RESPONSE" | jq -r '.issues[]? | "https://'"${JIRA_DOMAIN}"'/browse/\(.key)"' | head -10) + + if [ -z "$TICKETS" ]; then + echo "tickets=No linked tickets found for version: $JIRA_FIX_VERSION" >> $GITHUB_OUTPUT + else + echo "tickets<> $GITHUB_OUTPUT + echo "$TICKETS" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + fi + + - name: đŸ“ĸ Determine status + id: status + run: | + STATUS="${{ needs.create-prerelease.result }}" + + if [[ "$STATUS" == "success" ]]; then + echo "success=true" >> $GITHUB_OUTPUT + else + echo "success=false" >> $GITHUB_OUTPUT + fi + + - name: 📨 Send Slack notification (Success) + if: steps.status.outputs.success == 'true' + uses: slackapi/slack-github-action@v1 + with: + payload: | + { + "text": "\n:flutter::flutter::flutter::flutter::flutter::flutter::flutter::flutter::flutter::flutter::flutter::flutter:\n\n*Flutter Release Candidate:*\nappsflyer_sdk: ${{ needs.validate-release.outputs.version }} is ready for QA testing.\n\n*Testing Instructions:*\nAdd to pubspec.yaml:\n```\ndependencies:\n appsflyer_sdk:\n git:\n url: https://github.com/${{ github.repository }}.git\n ref: ${{ needs.validate-release.outputs.version }}\n```\n\n*Sources:*\n:github: https://github.com/${{ github.repository }}/tree/${{ github.ref_name }}\n:github: Release: https://github.com/${{ github.repository }}/releases/tag/${{ needs.validate-release.outputs.version }}\n\n*Changes and fixes:*\n${{ steps.extract-info.outputs.changelog }}\n\n*Linked tickets and issues:*\n${{ steps.jira-tickets.outputs.tickets }}\n\n*Native SDK's:*\n:android: ${{ steps.extract-info.outputs.android_sdk }}\n:apple: ${{ steps.extract-info.outputs.ios_sdk }}\n\n*Purchase Connector:*\n:android: ${{ steps.extract-info.outputs.android_pc }}\n:apple: ${{ steps.extract-info.outputs.ios_pc }}\n\n:flutter::flutter::flutter::flutter::flutter::flutter::flutter::flutter::flutter::flutter::flutter::flutter:" + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_WEBHOOK_URL }} + + - name: 📨 Send failure notification + if: steps.status.outputs.success == 'false' + uses: slackapi/slack-github-action@v1 + with: + payload: | + { + "text": "\n:warning: *Flutter RC Creation Failed*\n\nVersion: ${{ needs.validate-release.outputs.version }}\nBranch: ${{ github.ref_name }}\n\nPlease check the workflow logs: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_WEBHOOK_URL }} + + # =========================================================================== + # Job 6: RC Summary + # =========================================================================== + # Provides a summary of the RC release process + # =========================================================================== + + rc-summary: + name: 📋 RC Summary + runs-on: ubuntu-latest + needs: [validate-release, run-ci, update-version, create-prerelease] + if: always() + + steps: + - name: 📊 Display RC Summary + run: | + echo "=========================================" + echo "RC Release Summary" + echo "=========================================" + echo "Version: ${{ needs.validate-release.outputs.version }}" + echo "Is RC: ${{ needs.validate-release.outputs.is_rc }}" + echo "Is Valid: ${{ needs.validate-release.outputs.is_valid }}" + echo "-----------------------------------------" + echo "Validation: ${{ needs.validate-release.result }}" + echo "CI Pipeline: ${{ needs.run-ci.result }}" + echo "Version Update: ${{ needs.update-version.result }}" + echo "Pre-Release: ${{ needs.create-prerelease.result }}" + echo "=========================================" + + # Check if all critical jobs succeeded + if [[ "${{ needs.validate-release.result }}" == "success" ]] && \ + [[ "${{ needs.create-prerelease.result }}" == "success" ]]; then + echo "✅ RC Release Process Completed Successfully" + echo "" + echo "Next Steps:" + echo "1. Notify QA team to begin testing" + echo "2. Test the RC version thoroughly" + echo "3. If approved, proceed with production release" + else + echo "❌ RC Release Process Failed" + echo "Please check the logs above for details" + exit 1 + fi From 43d01e9ac06db56dca66ac1c8866c1e475dda615 Mon Sep 17 00:00:00 2001 From: Dani Koza <103039399+Dani-Koza-AF@users.noreply.github.com> Date: Wed, 15 Oct 2025 12:35:34 +0300 Subject: [PATCH 33/42] test: verify CI workflow (#420) * test: verify CI workflow * fixed Flutter Version determination * flutter analyze fixes * more flutter analyze fixes for example app * updated flutter analyze command * format code with dart format * Fix iOS CI order and increase gradle heap size * putting back important file for build testing with iOS * example project update Podfile --- .github/workflows/ci.yml | 8 +- .github/workflows/production-release.yml | 1 - .github/workflows/rc-release.yml | 1 - .gitignore | 2 - example/android/gradle.properties | 2 +- example/ios/Podfile | 4 +- example/ios/Runner.xcodeproj/project.pbxproj | 752 ++++++++++++++++++ example/lib/app_constants.dart | 4 +- example/lib/home_container.dart | 12 +- example/lib/home_container_streams.dart | 78 +- example/lib/main.dart | 8 +- example/lib/main_page.dart | 2 + example/lib/text_border.dart | 24 +- example/lib/utils.dart | 2 +- example/pubspec.yaml | 1 + lib/src/appsflyer_ad_revenue_data.dart | 13 +- lib/src/appsflyer_consent.dart | 17 +- lib/src/appsflyer_invite_link_params.dart | 19 +- lib/src/appsflyer_request_listener.dart | 2 +- lib/src/appsflyer_sdk.dart | 5 +- .../connector_callbacks.dart | 2 + .../missing_configuration_exception.dart | 1 + .../in_app_purchase_validation_result.dart | 22 +- .../purchase_connector/models/ios_error.dart | 8 +- .../models/jvm_throwable.dart | 8 +- .../models/product_purchase.dart | 12 +- .../models/subscription_purchase.dart | 197 ++--- .../subscription_validation_result.dart | 20 +- .../models/validation_failure_data.dart | 14 +- lib/src/udl/deep_link_result.dart | 36 +- lib/src/udl/deeplink.dart | 48 +- test/appsflyer_sdk_test.dart | 16 - 32 files changed, 977 insertions(+), 364 deletions(-) create mode 100644 example/ios/Runner.xcodeproj/project.pbxproj diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc3724b9..e41b0c2b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,8 +76,7 @@ jobs: - name: 🔧 Setup Flutter SDK uses: subosito/flutter-action@v2 with: - flutter-version: 'stable' # Use latest stable Flutter - channel: 'stable' + channel: 'stable' # Use latest stable Flutter cache: true # Cache Flutter SDK for faster runs # Step 3: Verify Flutter installation @@ -93,8 +92,9 @@ jobs: # Step 5: Analyze code for issues # Checks for code quality issues, unused imports, etc. + # Only fails on actual errors, not info-level warnings - name: 🔍 Analyze code - run: flutter analyze + run: flutter analyze --no-fatal-infos --no-fatal-warnings # Step 6: Format check # Ensures code follows Dart formatting standards @@ -145,7 +145,6 @@ jobs: - name: 🔧 Setup Flutter SDK uses: subosito/flutter-action@v2 with: - flutter-version: 'stable' channel: 'stable' cache: true @@ -209,7 +208,6 @@ jobs: - name: 🔧 Setup Flutter SDK uses: subosito/flutter-action@v2 with: - flutter-version: 'stable' channel: 'stable' cache: true diff --git a/.github/workflows/production-release.yml b/.github/workflows/production-release.yml index 50a87c65..e4a7af8d 100644 --- a/.github/workflows/production-release.yml +++ b/.github/workflows/production-release.yml @@ -191,7 +191,6 @@ jobs: - name: 🔧 Setup Flutter SDK uses: subosito/flutter-action@v2 with: - flutter-version: 'stable' channel: 'stable' cache: true diff --git a/.github/workflows/rc-release.yml b/.github/workflows/rc-release.yml index 628463d4..a31075d4 100644 --- a/.github/workflows/rc-release.yml +++ b/.github/workflows/rc-release.yml @@ -173,7 +173,6 @@ jobs: - name: 🔧 Setup Flutter SDK uses: subosito/flutter-action@v2 with: - flutter-version: 'stable' channel: 'stable' cache: true diff --git a/.gitignore b/.gitignore index db41b259..bd18aee4 100644 --- a/.gitignore +++ b/.gitignore @@ -93,8 +93,6 @@ android/\.classpath example/ios/Flutter/flutter_export_environment.sh -example/ios/Runner.xcodeproj/project.pbxproj - example/.flutter-plugins-dependencies example/macos/* diff --git a/example/android/gradle.properties b/example/android/gradle.properties index 94adc3a3..d74c11a1 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4096M -XX:MaxMetaspaceSize=1024m -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true android.enableJetifier=true diff --git a/example/ios/Podfile b/example/ios/Podfile index a72f9d72..9a3af86b 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '12.0' +platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -41,7 +41,7 @@ post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) target.build_configurations.each do |config| - config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0' + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0' end end end diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..bbde4090 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,752 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 76EE207FCCACA26B86D9DB82 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 64AB0D3E7E64D32945839514 /* Pods_RunnerTests.framework */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; + 8BCD5DFAB75A12A30E211076 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5039C24645EBF280C2BA493C /* Pods_Runner.framework */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 5039C24645EBF280C2BA493C /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 5BCE328CFF9133F9ED21F991 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 64AB0D3E7E64D32945839514 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 9E8B4CB342EC3B37664BA0D8 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + B136DB7A9307DF7BCA0C7FBB /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + C9F42F6C3D50967F3C103876 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + F2807C307C2224904744BDAB /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + FB7689F8FA4D75CF997E4F5C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 2892185B0C8A362FF17F84E0 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 76EE207FCCACA26B86D9DB82 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, + 8BCD5DFAB75A12A30E211076 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9394675F9B08C1249B8F3C96 /* Pods */ = { + isa = PBXGroup; + children = ( + F2807C307C2224904744BDAB /* Pods-Runner.debug.xcconfig */, + FB7689F8FA4D75CF997E4F5C /* Pods-Runner.release.xcconfig */, + B136DB7A9307DF7BCA0C7FBB /* Pods-Runner.profile.xcconfig */, + 9E8B4CB342EC3B37664BA0D8 /* Pods-RunnerTests.debug.xcconfig */, + C9F42F6C3D50967F3C103876 /* Pods-RunnerTests.release.xcconfig */, + 5BCE328CFF9133F9ED21F991 /* Pods-RunnerTests.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + 9394675F9B08C1249B8F3C96 /* Pods */, + B99305DCE4DB856ED0A78A75 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + B99305DCE4DB856ED0A78A75 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 5039C24645EBF280C2BA493C /* Pods_Runner.framework */, + 64AB0D3E7E64D32945839514 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + FEEC7F8166AF9F7CB31591F0 /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + 2892185B0C8A362FF17F84E0 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + C890491DE91035C837CBA73B /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + D2F636E2006BF2310FF8EF27 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + ); + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + C890491DE91035C837CBA73B /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + D2F636E2006BF2310FF8EF27 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + FEEC7F8166AF9F7CB31591F0 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 6UQAD4B3U2; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.appsflyer.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9E8B4CB342EC3B37664BA0D8 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.appsflyer.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = C9F42F6C3D50967F3C103876 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.appsflyer.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5BCE328CFF9133F9ED21F991 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.appsflyer.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 6UQAD4B3U2; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.appsflyer.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 6UQAD4B3U2; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.appsflyer.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/example/lib/app_constants.dart b/example/lib/app_constants.dart index 1f040969..a3641a3c 100644 --- a/example/lib/app_constants.dart +++ b/example/lib/app_constants.dart @@ -1,4 +1,4 @@ class AppConstants { - static const double TOP_PADDING = 12.0; - static const double CONTAINER_PADDING = 8.0; + static const double topPadding = 12.0; + static const double containerPadding = 8.0; } diff --git a/example/lib/home_container.dart b/example/lib/home_container.dart index 5a6b3f55..78c6c93b 100644 --- a/example/lib/home_container.dart +++ b/example/lib/home_container.dart @@ -9,18 +9,20 @@ class HomeContainer extends StatefulWidget { final Future Function(String, Map) logEvent; final void Function() logAdRevenueEvent; final Future?> Function(String, String) validatePurchase; - Object deepLinkData; + final Object? deepLinkData; + // ignore: prefer_const_constructors_in_immutables HomeContainer({ + Key? key, required this.onData, required this.deepLinkData, required this.logEvent, required this.logAdRevenueEvent, required this.validatePurchase, - }); + }) : super(key: key); @override - _HomeContainerState createState() => _HomeContainerState(); + State createState() => _HomeContainerState(); } class _HomeContainerState extends State { @@ -52,7 +54,7 @@ class _HomeContainerState extends State { Widget build(BuildContext context) { return SingleChildScrollView( child: Container( - padding: const EdgeInsets.all(AppConstants.CONTAINER_PADDING), + padding: const EdgeInsets.all(AppConstants.containerPadding), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -73,7 +75,7 @@ class _HomeContainerState extends State { fontWeight: FontWeight.w500, ), ), - SizedBox(height: AppConstants.TOP_PADDING), + SizedBox(height: AppConstants.topPadding), TextBorder( controller: TextEditingController( text: widget.onData.isNotEmpty diff --git a/example/lib/home_container_streams.dart b/example/lib/home_container_streams.dart index b9c22d57..ccfc962e 100644 --- a/example/lib/home_container_streams.dart +++ b/example/lib/home_container_streams.dart @@ -9,14 +9,16 @@ class HomeContainerStreams extends StatefulWidget { final Stream onAttribution; final Future Function(String, Map) logEvent; + // ignore: prefer_const_constructors_in_immutables HomeContainerStreams({ + Key? key, required this.onData, required this.onAttribution, - required this.logEvent - }); + required this.logEvent, + }) : super(key: key); @override - _HomeContainerStreamsState createState() => _HomeContainerStreamsState(); + State createState() => _HomeContainerStreamsState(); } class _HomeContainerStreamsState extends State { @@ -28,30 +30,28 @@ class _HomeContainerStreamsState extends State { "af_revenue": "2" }; - String _logEventResponse = "Event status will be shown here once it's triggered."; + String _logEventResponse = + "Event status will be shown here once it's triggered."; @override Widget build(BuildContext context) { return SingleChildScrollView( child: Container( - padding: EdgeInsets.all(AppConstants.CONTAINER_PADDING), + padding: const EdgeInsets.all(AppConstants.containerPadding), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( - padding: EdgeInsets.all(20.0), + padding: const EdgeInsets.all(20.0), decoration: BoxDecoration( color: Colors.white, - border: Border.all( - color: Colors.blueGrey, - width: 0.5 - ), + border: Border.all(color: Colors.blueGrey, width: 0.5), borderRadius: BorderRadius.circular(5), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( + const Text( "APPSFLYER SDK", style: TextStyle( fontSize: 18, @@ -59,65 +59,64 @@ class _HomeContainerStreamsState extends State { fontWeight: FontWeight.w500, ), ), - SizedBox(height: AppConstants.TOP_PADDING), + const SizedBox(height: AppConstants.topPadding), StreamBuilder( stream: widget.onData.asBroadcastStream(), - builder: (BuildContext context, AsyncSnapshot snapshot) { + builder: (BuildContext context, + AsyncSnapshot snapshot) { return TextBorder( controller: TextEditingController( - text: snapshot.hasData ? Utils.formatJson(snapshot.data) : "Waiting for conversion data..." - ), + text: snapshot.hasData + ? Utils.formatJson(snapshot.data) + : "Waiting for conversion data..."), labelText: "CONVERSION DATA", ); }, ), - SizedBox(height: 12.0), + const SizedBox(height: 12.0), StreamBuilder( stream: widget.onAttribution.asBroadcastStream(), - builder: (BuildContext context, AsyncSnapshot snapshot) { + builder: (BuildContext context, + AsyncSnapshot snapshot) { return TextBorder( controller: TextEditingController( - text: snapshot.hasData ? Utils.formatJson(snapshot.data) : "Waiting for attribution data..." - ), + text: snapshot.hasData + ? Utils.formatJson(snapshot.data) + : "Waiting for attribution data..."), labelText: "ATTRIBUTION DATA", ); - } - ), + }), ], - ) - ), - SizedBox(height: 12.0), + )), + const SizedBox(height: 12.0), Container( - padding: EdgeInsets.all(20.0), + padding: const EdgeInsets.all(20.0), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(5), - border: Border.all( - color: Colors.grey, - width: 0.5 - ), + border: Border.all(color: Colors.grey, width: 0.5), ), child: Column(children: [ - Text( + const Text( "EVENT LOGGER", style: TextStyle( fontSize: 18, fontWeight: FontWeight.w500, ), ), - SizedBox(height: 12.0), + const SizedBox(height: 12.0), TextBorder( controller: TextEditingController( - text: "Event Name: $eventName\nEvent Values: $eventValues" - ), + text: + "Event Name: $eventName\nEvent Values: $eventValues"), labelText: "EVENT REQUEST", ), - SizedBox(height: 12.0), + const SizedBox(height: 12.0), TextBorder( labelText: "SERVER RESPONSE", controller: TextEditingController(text: _logEventResponse), ), - SizedBox(height: 20), + const SizedBox(height: 20), ElevatedButton( onPressed: () { widget.logEvent(eventName, eventValues).then((onValue) { @@ -130,15 +129,16 @@ class _HomeContainerStreamsState extends State { }); }); }, - child: Text("Trigger Event"), style: ElevatedButton.styleFrom( backgroundColor: Colors.white, - padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10), - textStyle: TextStyle( + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 10), + textStyle: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ), + child: const Text("Trigger Event"), ), ]), ) @@ -147,4 +147,4 @@ class _HomeContainerStreamsState extends State { ), ); } -} \ No newline at end of file +} diff --git a/example/lib/main.dart b/example/lib/main.dart index a3e17ee4..82ec4acd 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -7,14 +7,16 @@ import './main_page.dart'; Future main() async { await dotenv.load(fileName: '.env'); - runApp(MyApp()); + runApp(const MyApp()); } class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { - return new MaterialApp( - home: new MainPage(), + return const MaterialApp( + home: MainPage(), ); } } diff --git a/example/lib/main_page.dart b/example/lib/main_page.dart index 3cc37d42..7e66d77b 100644 --- a/example/lib/main_page.dart +++ b/example/lib/main_page.dart @@ -7,6 +7,8 @@ import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'home_container.dart'; class MainPage extends StatefulWidget { + const MainPage({Key? key}) : super(key: key); + @override State createState() { return MainPageState(); diff --git a/example/lib/text_border.dart b/example/lib/text_border.dart index 66433521..cdcb9130 100644 --- a/example/lib/text_border.dart +++ b/example/lib/text_border.dart @@ -5,35 +5,31 @@ class TextBorder extends StatelessWidget { final TextEditingController controller; final String labelText; - const TextBorder({ - required this.controller, - required this.labelText, - Key? key - }) : super(key: key); + const TextBorder( + {required this.controller, required this.labelText, Key? key}) + : super(key: key); @override Widget build(BuildContext context) { return Container( - margin: const EdgeInsets.symmetric(vertical: 8.0), // Add some vertical margin + margin: + const EdgeInsets.symmetric(vertical: 8.0), // Add some vertical margin child: TextField( controller: controller, enabled: false, maxLines: null, decoration: InputDecoration( labelText: labelText, - labelStyle: TextStyle(color: Colors.blueGrey), // Change the color of the label + labelStyle: TextStyle( + color: Colors.blueGrey), // Change the color of the label border: OutlineInputBorder( - borderSide: BorderSide( - width: 1.0 - ), + borderSide: BorderSide(width: 1.0), ), focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - width: 1.0 - ), + borderSide: BorderSide(width: 1.0), ), ), ), ); } -} \ No newline at end of file +} diff --git a/example/lib/utils.dart b/example/lib/utils.dart index 3eae47cc..8b89728d 100644 --- a/example/lib/utils.dart +++ b/example/lib/utils.dart @@ -3,7 +3,7 @@ import 'dart:convert'; class Utils { static String formatJson(jsonObj) { // ignore: prefer_final_locals - JsonEncoder encoder = new JsonEncoder.withIndent(' '); + JsonEncoder encoder = const JsonEncoder.withIndent(' '); return encoder.convert(jsonObj); } } diff --git a/example/pubspec.yaml b/example/pubspec.yaml index fc8901fa..c33344d1 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -28,6 +28,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + flutter_lints: ^4.0.0 # For information on the generic Dart part of this file, see the # following page: https://www.dartlang.org/tools/pub/pubspec diff --git a/lib/src/appsflyer_ad_revenue_data.dart b/lib/src/appsflyer_ad_revenue_data.dart index 7482cf7b..c759caa0 100644 --- a/lib/src/appsflyer_ad_revenue_data.dart +++ b/lib/src/appsflyer_ad_revenue_data.dart @@ -7,13 +7,12 @@ class AdRevenueData { final double revenue; final Map? additionalParameters; - AdRevenueData({ - required this.monetizationNetwork, - required this.mediationNetwork, - required this.currencyIso4217Code, - required this.revenue, - this.additionalParameters - }); + AdRevenueData( + {required this.monetizationNetwork, + required this.mediationNetwork, + required this.currencyIso4217Code, + required this.revenue, + this.additionalParameters}); Map toMap() { return { diff --git a/lib/src/appsflyer_consent.dart b/lib/src/appsflyer_consent.dart index 300c7081..416ce67d 100644 --- a/lib/src/appsflyer_consent.dart +++ b/lib/src/appsflyer_consent.dart @@ -12,23 +12,20 @@ class AppsFlyerConsent { }); // Factory constructors - factory AppsFlyerConsent.forGDPRUser({ - required bool hasConsentForDataUsage, - required bool hasConsentForAdsPersonalization - }){ + factory AppsFlyerConsent.forGDPRUser( + {required bool hasConsentForDataUsage, + required bool hasConsentForAdsPersonalization}) { return AppsFlyerConsent._( isUserSubjectToGDPR: true, hasConsentForDataUsage: hasConsentForDataUsage, - hasConsentForAdsPersonalization: hasConsentForAdsPersonalization - ); + hasConsentForAdsPersonalization: hasConsentForAdsPersonalization); } - factory AppsFlyerConsent.nonGDPRUser(){ + factory AppsFlyerConsent.nonGDPRUser() { return AppsFlyerConsent._( isUserSubjectToGDPR: false, hasConsentForDataUsage: false, - hasConsentForAdsPersonalization: false - ); + hasConsentForAdsPersonalization: false); } // Converts object to a map @@ -39,4 +36,4 @@ class AppsFlyerConsent { 'hasConsentForAdsPersonalization': hasConsentForAdsPersonalization, }; } -} \ No newline at end of file +} diff --git a/lib/src/appsflyer_invite_link_params.dart b/lib/src/appsflyer_invite_link_params.dart index 772d23c1..43d8e32b 100644 --- a/lib/src/appsflyer_invite_link_params.dart +++ b/lib/src/appsflyer_invite_link_params.dart @@ -14,14 +14,13 @@ class AppsFlyerInviteLinkParams { /// Creates an [AppsFlyerInviteLinkParams] instance. /// All parameters are optional, allowing greater flexibility when /// invoking the constructor. - AppsFlyerInviteLinkParams({ - this.campaign, - this.channel, - this.referrerName, - this.baseDeepLink, - this.brandDomain, - this.customerID, - this.referrerImageUrl, - this.customParams - }); + AppsFlyerInviteLinkParams( + {this.campaign, + this.channel, + this.referrerName, + this.baseDeepLink, + this.brandDomain, + this.customerID, + this.referrerImageUrl, + this.customParams}); } diff --git a/lib/src/appsflyer_request_listener.dart b/lib/src/appsflyer_request_listener.dart index 3bf8f191..45e6af6c 100644 --- a/lib/src/appsflyer_request_listener.dart +++ b/lib/src/appsflyer_request_listener.dart @@ -8,4 +8,4 @@ class AppsFlyerRequestListener { required this.onSuccess, required this.onError, }); -} \ No newline at end of file +} diff --git a/lib/src/appsflyer_sdk.dart b/lib/src/appsflyer_sdk.dart index 5336fa44..50e54723 100644 --- a/lib/src/appsflyer_sdk.dart +++ b/lib/src/appsflyer_sdk.dart @@ -1,6 +1,7 @@ part of appsflyer_sdk; class AppsflyerSdk { + // ignore: unused_field EventChannel _eventChannel; static AppsflyerSdk? _instance; final MethodChannel _methodChannel; @@ -9,8 +10,8 @@ class AppsflyerSdk { AppsFlyerOptions? afOptions; Map? mapOptions; - ///Returns the [AppsflyerSdk] instance, initialized with a custom options - ///provided by the user + /// Returns the [AppsflyerSdk] instance, initialized with a custom options + /// provided by the user factory AppsflyerSdk(options) { if (_instance == null) { MethodChannel methodChannel = diff --git a/lib/src/purchase_connector/connector_callbacks.dart b/lib/src/purchase_connector/connector_callbacks.dart index c908cfea..554e504a 100644 --- a/lib/src/purchase_connector/connector_callbacks.dart +++ b/lib/src/purchase_connector/connector_callbacks.dart @@ -2,9 +2,11 @@ part of appsflyer_sdk; /// Type definition for a general purpose listener. typedef PurchaseConnectorListener = Function(dynamic); + /// Type definition for a listener which gets called when the `PurchaseConnectorImpl` receives purchase revenue validation info for iOS. typedef DidReceivePurchaseRevenueValidationInfo = Function( Map? validationInfo, IosError? error); + /// Invoked when a 200 OK response is received from the server. /// Note: An INVALID purchase is considered to be a successful response and will also be returned by this callback. /// diff --git a/lib/src/purchase_connector/missing_configuration_exception.dart b/lib/src/purchase_connector/missing_configuration_exception.dart index eae23676..8d104585 100644 --- a/lib/src/purchase_connector/missing_configuration_exception.dart +++ b/lib/src/purchase_connector/missing_configuration_exception.dart @@ -1,4 +1,5 @@ part of appsflyer_sdk; + /// Exception thrown when the configuration is missing. class MissingConfigurationException implements Exception { final String message; diff --git a/lib/src/purchase_connector/models/in_app_purchase_validation_result.dart b/lib/src/purchase_connector/models/in_app_purchase_validation_result.dart index 0846a180..4cf6e4ae 100644 --- a/lib/src/purchase_connector/models/in_app_purchase_validation_result.dart +++ b/lib/src/purchase_connector/models/in_app_purchase_validation_result.dart @@ -2,32 +2,28 @@ part of appsflyer_sdk; @JsonSerializable() class InAppPurchaseValidationResult { - bool success; ProductPurchase? productPurchase; ValidationFailureData? failureData; InAppPurchaseValidationResult( - this.success, - this.productPurchase, - this.failureData - ); - - + this.success, this.productPurchase, this.failureData); - factory InAppPurchaseValidationResult.fromJson(Map json) => _$InAppPurchaseValidationResultFromJson(json); + factory InAppPurchaseValidationResult.fromJson(Map json) => + _$InAppPurchaseValidationResultFromJson(json); Map toJson() => _$InAppPurchaseValidationResultToJson(this); - } @JsonSerializable() -class InAppPurchaseValidationResultMap{ +class InAppPurchaseValidationResultMap { Map result; InAppPurchaseValidationResultMap(this.result); - factory InAppPurchaseValidationResultMap.fromJson(Map json) => _$InAppPurchaseValidationResultMapFromJson(json); - - Map toJson() => _$InAppPurchaseValidationResultMapToJson(this); + factory InAppPurchaseValidationResultMap.fromJson( + Map json) => + _$InAppPurchaseValidationResultMapFromJson(json); + Map toJson() => + _$InAppPurchaseValidationResultMapToJson(this); } diff --git a/lib/src/purchase_connector/models/ios_error.dart b/lib/src/purchase_connector/models/ios_error.dart index aa671066..fd9b1a72 100644 --- a/lib/src/purchase_connector/models/ios_error.dart +++ b/lib/src/purchase_connector/models/ios_error.dart @@ -1,15 +1,15 @@ part of appsflyer_sdk; @JsonSerializable() -class IosError{ +class IosError { String localizedDescription; String domain; int code; - IosError(this.localizedDescription, this.domain, this.code); - factory IosError.fromJson(Map json) => _$IosErrorFromJson(json); + factory IosError.fromJson(Map json) => + _$IosErrorFromJson(json); Map toJson() => _$IosErrorToJson(this); -} \ No newline at end of file +} diff --git a/lib/src/purchase_connector/models/jvm_throwable.dart b/lib/src/purchase_connector/models/jvm_throwable.dart index 04fb07e5..d5878192 100644 --- a/lib/src/purchase_connector/models/jvm_throwable.dart +++ b/lib/src/purchase_connector/models/jvm_throwable.dart @@ -1,7 +1,7 @@ part of appsflyer_sdk; @JsonSerializable() -class JVMThrowable{ +class JVMThrowable { String type; String message; String stacktrace; @@ -9,8 +9,8 @@ class JVMThrowable{ JVMThrowable(this.type, this.message, this.stacktrace, this.cause); - factory JVMThrowable.fromJson(Map json) => _$JVMThrowableFromJson(json); + factory JVMThrowable.fromJson(Map json) => + _$JVMThrowableFromJson(json); Map toJson() => _$JVMThrowableToJson(this); - -} \ No newline at end of file +} diff --git a/lib/src/purchase_connector/models/product_purchase.dart b/lib/src/purchase_connector/models/product_purchase.dart index e6e3ceb6..9e7bcc2a 100644 --- a/lib/src/purchase_connector/models/product_purchase.dart +++ b/lib/src/purchase_connector/models/product_purchase.dart @@ -2,7 +2,6 @@ part of appsflyer_sdk; @JsonSerializable() class ProductPurchase { - String kind; String purchaseTimeMillis; int purchaseState; @@ -32,13 +31,10 @@ class ProductPurchase { this.quantity, this.obfuscatedExternalAccountId, this.obfuscatedExternalProfileId, - this.regionCode - ); - + this.regionCode); - - factory ProductPurchase.fromJson(Map json) => _$ProductPurchaseFromJson(json); + factory ProductPurchase.fromJson(Map json) => + _$ProductPurchaseFromJson(json); Map toJson() => _$ProductPurchaseToJson(this); - -} \ No newline at end of file +} diff --git a/lib/src/purchase_connector/models/subscription_purchase.dart b/lib/src/purchase_connector/models/subscription_purchase.dart index 55ede1e6..ec3b153a 100644 --- a/lib/src/purchase_connector/models/subscription_purchase.dart +++ b/lib/src/purchase_connector/models/subscription_purchase.dart @@ -2,7 +2,6 @@ part of appsflyer_sdk; @JsonSerializable() class SubscriptionPurchase { - String acknowledgementState; CanceledStateContext? canceledStateContext; ExternalAccountIdentifiers? externalAccountIdentifiers; @@ -30,21 +29,16 @@ class SubscriptionPurchase { this.startTime, this.subscribeWithGoogleInfo, this.subscriptionState, - this.testPurchase - ); - + this.testPurchase); - - factory SubscriptionPurchase.fromJson(Map json) => _$SubscriptionPurchaseFromJson(json); + factory SubscriptionPurchase.fromJson(Map json) => + _$SubscriptionPurchaseFromJson(json); Map toJson() => _$SubscriptionPurchaseToJson(this); - } - @JsonSerializable() class CanceledStateContext { - DeveloperInitiatedCancellation? developerInitiatedCancellation; ReplacementCancellation? replacementCancellation; SystemInitiatedCancellation? systemInitiatedCancellation; @@ -54,104 +48,84 @@ class CanceledStateContext { this.developerInitiatedCancellation, this.replacementCancellation, this.systemInitiatedCancellation, - this.userInitiatedCancellation - ); - - + this.userInitiatedCancellation); - factory CanceledStateContext.fromJson(Map json) => _$CanceledStateContextFromJson(json); + factory CanceledStateContext.fromJson(Map json) => + _$CanceledStateContextFromJson(json); Map toJson() => _$CanceledStateContextToJson(this); - } @JsonSerializable() -class DeveloperInitiatedCancellation{ +class DeveloperInitiatedCancellation { DeveloperInitiatedCancellation(); - factory DeveloperInitiatedCancellation.fromJson(Map json) => _$DeveloperInitiatedCancellationFromJson(json); + factory DeveloperInitiatedCancellation.fromJson(Map json) => + _$DeveloperInitiatedCancellationFromJson(json); Map toJson() => _$DeveloperInitiatedCancellationToJson(this); } @JsonSerializable() -class ReplacementCancellation{ +class ReplacementCancellation { ReplacementCancellation(); - factory ReplacementCancellation.fromJson(Map json) => _$ReplacementCancellationFromJson(json); + factory ReplacementCancellation.fromJson(Map json) => + _$ReplacementCancellationFromJson(json); Map toJson() => _$ReplacementCancellationToJson(this); } @JsonSerializable() -class SystemInitiatedCancellation{ +class SystemInitiatedCancellation { SystemInitiatedCancellation(); - factory SystemInitiatedCancellation.fromJson(Map json) => _$SystemInitiatedCancellationFromJson(json); + factory SystemInitiatedCancellation.fromJson(Map json) => + _$SystemInitiatedCancellationFromJson(json); Map toJson() => _$SystemInitiatedCancellationToJson(this); } - @JsonSerializable() class UserInitiatedCancellation { - CancelSurveyResult? cancelSurveyResult; String cancelTime; - UserInitiatedCancellation( - this.cancelSurveyResult, - this.cancelTime - ); - + UserInitiatedCancellation(this.cancelSurveyResult, this.cancelTime); - - factory UserInitiatedCancellation.fromJson(Map json) => _$UserInitiatedCancellationFromJson(json); + factory UserInitiatedCancellation.fromJson(Map json) => + _$UserInitiatedCancellationFromJson(json); Map toJson() => _$UserInitiatedCancellationToJson(this); - } @JsonSerializable() class CancelSurveyResult { - String reason; String reasonUserInput; - CancelSurveyResult( - this.reason, - this.reasonUserInput - ); - + CancelSurveyResult(this.reason, this.reasonUserInput); - - factory CancelSurveyResult.fromJson(Map json) => _$CancelSurveyResultFromJson(json); + factory CancelSurveyResult.fromJson(Map json) => + _$CancelSurveyResultFromJson(json); Map toJson() => _$CancelSurveyResultToJson(this); - } @JsonSerializable() class ExternalAccountIdentifiers { - String externalAccountId; String obfuscatedExternalAccountId; String obfuscatedExternalProfileId; - ExternalAccountIdentifiers( - this.externalAccountId, - this.obfuscatedExternalAccountId, - this.obfuscatedExternalProfileId - ); - + ExternalAccountIdentifiers(this.externalAccountId, + this.obfuscatedExternalAccountId, this.obfuscatedExternalProfileId); - - factory ExternalAccountIdentifiers.fromJson(Map json) => _$ExternalAccountIdentifiersFromJson(json); + factory ExternalAccountIdentifiers.fromJson(Map json) => + _$ExternalAccountIdentifiersFromJson(json); Map toJson() => _$ExternalAccountIdentifiersToJson(this); - } @JsonSerializable() class SubscriptionPurchaseLineItem { - AutoRenewingPlan? autoRenewingPlan; DeferredItemReplacement? deferredItemReplacement; String expiryTime; @@ -165,179 +139,130 @@ class SubscriptionPurchaseLineItem { this.expiryTime, this.offerDetails, this.prepaidPlan, - this.productId - ); - + this.productId); - - factory SubscriptionPurchaseLineItem.fromJson(Map json) => _$SubscriptionPurchaseLineItemFromJson(json); + factory SubscriptionPurchaseLineItem.fromJson(Map json) => + _$SubscriptionPurchaseLineItemFromJson(json); Map toJson() => _$SubscriptionPurchaseLineItemToJson(this); - } @JsonSerializable() class OfferDetails { - List? offerTags; String basePlanId; String? offerId; - OfferDetails( - this.offerTags, - this.basePlanId, - this.offerId - ); - + OfferDetails(this.offerTags, this.basePlanId, this.offerId); - - factory OfferDetails.fromJson(Map json) => _$OfferDetailsFromJson(json); + factory OfferDetails.fromJson(Map json) => + _$OfferDetailsFromJson(json); Map toJson() => _$OfferDetailsToJson(this); - } @JsonSerializable() class AutoRenewingPlan { - bool? autoRenewEnabled; SubscriptionItemPriceChangeDetails? priceChangeDetails; - AutoRenewingPlan( - this.autoRenewEnabled, - this.priceChangeDetails - ); + AutoRenewingPlan(this.autoRenewEnabled, this.priceChangeDetails); - - - factory AutoRenewingPlan.fromJson(Map json) => _$AutoRenewingPlanFromJson(json); + factory AutoRenewingPlan.fromJson(Map json) => + _$AutoRenewingPlanFromJson(json); Map toJson() => _$AutoRenewingPlanToJson(this); - } @JsonSerializable() class SubscriptionItemPriceChangeDetails { - String expectedNewPriceChargeTime; Money? newPrice; String priceChangeMode; String priceChangeState; - SubscriptionItemPriceChangeDetails( - this.expectedNewPriceChargeTime, - this.newPrice, - this.priceChangeMode, - this.priceChangeState - ); + SubscriptionItemPriceChangeDetails(this.expectedNewPriceChargeTime, + this.newPrice, this.priceChangeMode, this.priceChangeState); + factory SubscriptionItemPriceChangeDetails.fromJson( + Map json) => + _$SubscriptionItemPriceChangeDetailsFromJson(json); - - factory SubscriptionItemPriceChangeDetails.fromJson(Map json) => _$SubscriptionItemPriceChangeDetailsFromJson(json); - - Map toJson() => _$SubscriptionItemPriceChangeDetailsToJson(this); - + Map toJson() => + _$SubscriptionItemPriceChangeDetailsToJson(this); } @JsonSerializable() class Money { - String currencyCode; int nanos; int units; - Money( - this.currencyCode, - this.nanos, - this.units - ); - - + Money(this.currencyCode, this.nanos, this.units); factory Money.fromJson(Map json) => _$MoneyFromJson(json); Map toJson() => _$MoneyToJson(this); - } + @JsonSerializable() class DeferredItemReplacement { - String productId; - DeferredItemReplacement( - this.productId - ); - - + DeferredItemReplacement(this.productId); - factory DeferredItemReplacement.fromJson(Map json) => _$DeferredItemReplacementFromJson(json); + factory DeferredItemReplacement.fromJson(Map json) => + _$DeferredItemReplacementFromJson(json); Map toJson() => _$DeferredItemReplacementToJson(this); - } @JsonSerializable() class PrepaidPlan { - String? allowExtendAfterTime; - PrepaidPlan( - this.allowExtendAfterTime - ); - - + PrepaidPlan(this.allowExtendAfterTime); - factory PrepaidPlan.fromJson(Map json) => _$PrepaidPlanFromJson(json); + factory PrepaidPlan.fromJson(Map json) => + _$PrepaidPlanFromJson(json); Map toJson() => _$PrepaidPlanToJson(this); - } @JsonSerializable() class PausedStateContext { - String autoResumeTime; - PausedStateContext( - this.autoResumeTime - ); - - + PausedStateContext(this.autoResumeTime); - factory PausedStateContext.fromJson(Map json) => _$PausedStateContextFromJson(json); + factory PausedStateContext.fromJson(Map json) => + _$PausedStateContextFromJson(json); Map toJson() => _$PausedStateContextToJson(this); - } + @JsonSerializable() class SubscribeWithGoogleInfo { - String emailAddress; String familyName; String givenName; String profileId; String profileName; - SubscribeWithGoogleInfo( - this.emailAddress, - this.familyName, - this.givenName, - this.profileId, - this.profileName - ); - + SubscribeWithGoogleInfo(this.emailAddress, this.familyName, this.givenName, + this.profileId, this.profileName); - - factory SubscribeWithGoogleInfo.fromJson(Map json) => _$SubscribeWithGoogleInfoFromJson(json); + factory SubscribeWithGoogleInfo.fromJson(Map json) => + _$SubscribeWithGoogleInfoFromJson(json); Map toJson() => _$SubscribeWithGoogleInfoToJson(this); - } @JsonSerializable() -class TestPurchase{ +class TestPurchase { TestPurchase(); - factory TestPurchase.fromJson(Map json) => _$TestPurchaseFromJson(json); + factory TestPurchase.fromJson(Map json) => + _$TestPurchaseFromJson(json); Map toJson() => _$TestPurchaseToJson(this); -} \ No newline at end of file +} diff --git a/lib/src/purchase_connector/models/subscription_validation_result.dart b/lib/src/purchase_connector/models/subscription_validation_result.dart index b9294176..4790b03e 100644 --- a/lib/src/purchase_connector/models/subscription_validation_result.dart +++ b/lib/src/purchase_connector/models/subscription_validation_result.dart @@ -2,31 +2,27 @@ part of appsflyer_sdk; @JsonSerializable() class SubscriptionValidationResult { - bool success; SubscriptionPurchase? subscriptionPurchase; ValidationFailureData? failureData; SubscriptionValidationResult( - this.success, - this.subscriptionPurchase, - this.failureData - ); - + this.success, this.subscriptionPurchase, this.failureData); - - factory SubscriptionValidationResult.fromJson(Map json) => _$SubscriptionValidationResultFromJson(json); + factory SubscriptionValidationResult.fromJson(Map json) => + _$SubscriptionValidationResultFromJson(json); Map toJson() => _$SubscriptionValidationResultToJson(this); - } @JsonSerializable() -class SubscriptionValidationResultMap{ +class SubscriptionValidationResultMap { Map result; SubscriptionValidationResultMap(this.result); - factory SubscriptionValidationResultMap.fromJson(Map json) => _$SubscriptionValidationResultMapFromJson(json); + factory SubscriptionValidationResultMap.fromJson(Map json) => + _$SubscriptionValidationResultMapFromJson(json); - Map toJson() => _$SubscriptionValidationResultMapToJson(this); + Map toJson() => + _$SubscriptionValidationResultMapToJson(this); } diff --git a/lib/src/purchase_connector/models/validation_failure_data.dart b/lib/src/purchase_connector/models/validation_failure_data.dart index 57b3d137..5da921f5 100644 --- a/lib/src/purchase_connector/models/validation_failure_data.dart +++ b/lib/src/purchase_connector/models/validation_failure_data.dart @@ -2,19 +2,13 @@ part of appsflyer_sdk; @JsonSerializable() class ValidationFailureData { - int status; String description; - ValidationFailureData( - this.status, - this.description - ); - + ValidationFailureData(this.status, this.description); - - factory ValidationFailureData.fromJson(Map json) => _$ValidationFailureDataFromJson(json); + factory ValidationFailureData.fromJson(Map json) => + _$ValidationFailureDataFromJson(json); Map toJson() => _$ValidationFailureDataToJson(this); - -} \ No newline at end of file +} diff --git a/lib/src/udl/deep_link_result.dart b/lib/src/udl/deep_link_result.dart index 388dada7..9cfcefd4 100644 --- a/lib/src/udl/deep_link_result.dart +++ b/lib/src/udl/deep_link_result.dart @@ -1,6 +1,5 @@ part of appsflyer_sdk; - class DeepLinkResult { final Error? _error; final DeepLink? _deepLink; @@ -19,8 +18,7 @@ class DeepLinkResult { _status = json['status'], _deepLink = json['deepLink']; - Map toJson() => - { + Map toJson() => { 'status': _status.toShortString(), 'error': _error?.toShortString(), 'deepLink': _deepLink?.clickEvent, @@ -30,23 +28,11 @@ class DeepLinkResult { String toString() { return "DeepLinkResult:${jsonEncode(toJson())}"; } - - } -enum Error { - TIMEOUT, - NETWORK, - HTTP_STATUS_CODE, - UNEXPECTED, - DEVELOPER_ERROR -} -enum Status { - FOUND, - NOT_FOUND, - ERROR, - PARSE_ERROR -} +enum Error { TIMEOUT, NETWORK, HTTP_STATUS_CODE, UNEXPECTED, DEVELOPER_ERROR } + +enum Status { FOUND, NOT_FOUND, ERROR, PARSE_ERROR } extension ParseStatusToString on Status { String toShortString() { @@ -62,22 +48,22 @@ extension ParseErrorToString on Error { extension ParseEnumFromString on String { Status? statusFromString() { - return Status.values.firstWhere( - (s) => _describeEnum(s) == this, orElse: null); + return Status.values + .firstWhere((s) => _describeEnum(s) == this, orElse: null); } Error? errorFromString() { - return Error.values.firstWhere((e) => _describeEnum(e) == this, - orElse: null); + return Error.values + .firstWhere((e) => _describeEnum(e) == this, orElse: null); } String _describeEnum(Object enumEntry) { final String description = enumEntry.toString(); final int indexOfDot = description.indexOf('.'); assert( - indexOfDot != -1 && indexOfDot < description.length - 1, - 'The provided object "$enumEntry" is not an enum.', + indexOfDot != -1 && indexOfDot < description.length - 1, + 'The provided object "$enumEntry" is not an enum.', ); return description.substring(indexOfDot + 1); } -} \ No newline at end of file +} diff --git a/lib/src/udl/deeplink.dart b/lib/src/udl/deeplink.dart index 88fdaff3..40efe063 100644 --- a/lib/src/udl/deeplink.dart +++ b/lib/src/udl/deeplink.dart @@ -1,55 +1,43 @@ part of appsflyer_sdk; - -class DeepLink{ - - final Map _clickEvent; +class DeepLink { + final Map _clickEvent; DeepLink(this._clickEvent); - Map get clickEvent => _clickEvent; + Map get clickEvent => _clickEvent; String? getStringValue(String key) { return _clickEvent[key] as String?; } - String? get deepLinkValue => _clickEvent["deep_link_value"] as String?; - - - String? get matchType => _clickEvent["match_type"] as String?; - - - String? get clickHttpReferrer => _clickEvent["click_http_referrer"] as String?; - + String? get deepLinkValue => _clickEvent["deep_link_value"] as String?; - String? get mediaSource => _clickEvent["media_source"] as String?; + String? get matchType => _clickEvent["match_type"] as String?; + String? get clickHttpReferrer => + _clickEvent["click_http_referrer"] as String?; - String? get campaign => _clickEvent["campaign"] as String?; + String? get mediaSource => _clickEvent["media_source"] as String?; + String? get campaign => _clickEvent["campaign"] as String?; - String? get campaignId => _clickEvent["campaign_id"] as String?; - + String? get campaignId => _clickEvent["campaign_id"] as String?; String? get afSub1 => _clickEvent["af_sub1"] as String?; - - String? get afSub2 => _clickEvent["af_sub2"] as String?; - + String? get afSub2 => _clickEvent["af_sub2"] as String?; String? get afSub3 => _clickEvent["af_sub3"] as String?; + String? get afSub4 => _clickEvent["af_sub4"] as String?; - String? get afSub4 => _clickEvent["af_sub4"] as String?; - + String? get afSub5 => _clickEvent["af_sub5"] as String?; - String? get afSub5 => _clickEvent["af_sub5"] as String?; - - - bool? get isDeferred => _clickEvent["is_deferred"] as bool?; + bool? get isDeferred => _clickEvent["is_deferred"] as bool?; @override - String toString() { - return 'DeepLink: ${jsonEncode(_clickEvent)}'; - } -} \ No newline at end of file + String toString() { + return 'DeepLink: ${jsonEncode(_clickEvent)}'; + } +} diff --git a/test/appsflyer_sdk_test.dart b/test/appsflyer_sdk_test.dart index bcd396e6..80f3262f 100644 --- a/test/appsflyer_sdk_test.dart +++ b/test/appsflyer_sdk_test.dart @@ -305,16 +305,6 @@ void main() { expect(capturedArguments['mediationNetwork'], 'applovin_max'); }); - test('check setConsentData call', () async { - final consentData = AppsFlyerConsent.forGDPRUser( - hasConsentForDataUsage: true, - hasConsentForAdsPersonalization: true, - ); - instance.setConsentData(consentData); - - expect(selectedMethod, 'setConsentData'); - }); - test('check enableTCFDataCollection call', () async { instance.enableTCFDataCollection(true); @@ -342,12 +332,6 @@ void main() { expect(capturedArguments, contains('https://example.com')); }); - test('check setPushNotification call', () async { - instance.setPushNotification(true); - - expect(selectedMethod, 'setPushNotification'); - }); - test('check sendPushNotificationData call', () async { instance.sendPushNotificationData({'key': 'value'}); From 67a015fc48aef88e4ecdbf5abbc26099a4e13b20 Mon Sep 17 00:00:00 2001 From: Dani-Koza-AF Date: Wed, 15 Oct 2025 14:46:20 +0300 Subject: [PATCH 34/42] CI fix - Jira ntegration --- .github/workflows/production-release.yml | 32 ++++++++++++++++++----- .github/workflows/rc-release.yml | 33 ++++++++++++++++++------ 2 files changed, 50 insertions(+), 15 deletions(-) diff --git a/.github/workflows/production-release.yml b/.github/workflows/production-release.yml index e4a7af8d..0d6a54c6 100644 --- a/.github/workflows/production-release.yml +++ b/.github/workflows/production-release.yml @@ -430,14 +430,21 @@ jobs: - name: đŸŽĢ Fetch Jira tickets id: jira-tickets + continue-on-error: true # Don't fail CI if Jira fetch fails run: | + set +e # Don't exit on errors + VERSION="${{ needs.validate-release.outputs.version }}" + # Use full version with 'v' prefix (matches your Jira convention) + # For production release, version is X.Y.Z without -rc suffix JIRA_FIX_VERSION="Flutter SDK v$VERSION" + echo "🔍 Looking for Jira tickets with fix version: $JIRA_FIX_VERSION" + # Check if Jira credentials are available if [[ -z "${{ secrets.CI_JIRA_EMAIL }}" ]] || [[ -z "${{ secrets.CI_JIRA_TOKEN }}" ]]; then echo "âš ī¸ Jira credentials not configured" - echo "tickets=No linked tickets (Jira not configured)" >> $GITHUB_OUTPUT + echo "tickets=No assigned fix version found" >> $GITHUB_OUTPUT exit 0 fi @@ -447,19 +454,30 @@ jobs: # Create auth header AUTH=$(echo -n "${{ secrets.CI_JIRA_EMAIL }}:${{ secrets.CI_JIRA_TOKEN }}" | base64) - # Query Jira API - RESPONSE=$(curl -s -X GET \ + # Query Jira API with error handling + RESPONSE=$(curl -s -w "\n%{http_code}" -X GET \ -H "Authorization: Basic $AUTH" \ -H "Content-Type: application/json" \ "https://${JIRA_DOMAIN}/rest/api/3/search?jql=fixVersion=\"${JIRA_FIX_VERSION}\"&fields=key,summary&maxResults=20") - # Extract ticket keys and create links - TICKETS=$(echo "$RESPONSE" | jq -r '.issues[]? | "https://'"${JIRA_DOMAIN}"'/browse/\(.key)"' | head -10) + HTTP_CODE=$(echo "$RESPONSE" | tail -n1) + BODY=$(echo "$RESPONSE" | sed '$d') + + if [[ "$HTTP_CODE" != "200" ]]; then + echo "âš ī¸ Jira API request failed with status $HTTP_CODE" + echo "tickets=No assigned fix version found" >> $GITHUB_OUTPUT + exit 0 + fi + + # Extract ticket keys and create links with summaries + TICKETS=$(echo "$BODY" | jq -r '.issues[]? | "â€ĸ https://'"${JIRA_DOMAIN}"'/browse/\(.key) - \(.fields.summary)"' 2>/dev/null | head -10) if [ -z "$TICKETS" ]; then - echo "tickets=No linked tickets found for version: $JIRA_FIX_VERSION" >> $GITHUB_OUTPUT + echo "â„šī¸ No linked tickets found for version: $JIRA_FIX_VERSION" + echo "tickets=No assigned fix version found" >> $GITHUB_OUTPUT else - # Format tickets as newline-separated list + echo "✅ Found Jira tickets:" + echo "$TICKETS" echo "tickets<> $GITHUB_OUTPUT echo "$TICKETS" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT diff --git a/.github/workflows/rc-release.yml b/.github/workflows/rc-release.yml index a31075d4..bed71185 100644 --- a/.github/workflows/rc-release.yml +++ b/.github/workflows/rc-release.yml @@ -393,33 +393,50 @@ jobs: - name: đŸŽĢ Fetch Jira tickets id: jira-tickets + continue-on-error: true # Don't fail CI if Jira fetch fails run: | + set +e # Don't exit on errors + VERSION="${{ needs.validate-release.outputs.version }}" - BASE_VERSION=$(echo "$VERSION" | sed 's/-rc[0-9]*$//') - JIRA_FIX_VERSION="Flutter SDK v$BASE_VERSION" + # Use full version with -rc suffix and 'v' prefix (matches your Jira convention) + JIRA_FIX_VERSION="Flutter SDK v$VERSION" + + echo "🔍 Looking for Jira tickets with fix version: $JIRA_FIX_VERSION" # Check if Jira credentials are available if [[ -z "${{ secrets.CI_JIRA_EMAIL }}" ]] || [[ -z "${{ secrets.CI_JIRA_TOKEN }}" ]]; then echo "âš ī¸ Jira credentials not configured" - echo "tickets=No linked tickets (Jira not configured)" >> $GITHUB_OUTPUT + echo "tickets=No assigned fix version found" >> $GITHUB_OUTPUT exit 0 fi JIRA_DOMAIN="${{ secrets.CI_JIRA_DOMAIN || 'appsflyer.atlassian.net' }}" AUTH=$(echo -n "${{ secrets.CI_JIRA_EMAIL }}:${{ secrets.CI_JIRA_TOKEN }}" | base64) - # Query Jira API - RESPONSE=$(curl -s -X GET \ + # Query Jira API with error handling + RESPONSE=$(curl -s -w "\n%{http_code}" -X GET \ -H "Authorization: Basic $AUTH" \ -H "Content-Type: application/json" \ "https://${JIRA_DOMAIN}/rest/api/3/search?jql=fixVersion=\"${JIRA_FIX_VERSION}\"&fields=key,summary&maxResults=20") - # Extract ticket keys and create links - TICKETS=$(echo "$RESPONSE" | jq -r '.issues[]? | "https://'"${JIRA_DOMAIN}"'/browse/\(.key)"' | head -10) + HTTP_CODE=$(echo "$RESPONSE" | tail -n1) + BODY=$(echo "$RESPONSE" | sed '$d') + + if [[ "$HTTP_CODE" != "200" ]]; then + echo "âš ī¸ Jira API request failed with status $HTTP_CODE" + echo "tickets=No assigned fix version found" >> $GITHUB_OUTPUT + exit 0 + fi + + # Extract ticket keys and create links with summaries + TICKETS=$(echo "$BODY" | jq -r '.issues[]? | "â€ĸ https://'"${JIRA_DOMAIN}"'/browse/\(.key) - \(.fields.summary)"' 2>/dev/null | head -10) if [ -z "$TICKETS" ]; then - echo "tickets=No linked tickets found for version: $JIRA_FIX_VERSION" >> $GITHUB_OUTPUT + echo "â„šī¸ No linked tickets found for version: $JIRA_FIX_VERSION" + echo "tickets=No assigned fix version found" >> $GITHUB_OUTPUT else + echo "✅ Found Jira tickets:" + echo "$TICKETS" echo "tickets<> $GITHUB_OUTPUT echo "$TICKETS" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT From b362556c515e2223ecdb2aeca0f1dc1f0440a01a Mon Sep 17 00:00:00 2001 From: Dani-Koza-AF Date: Wed, 15 Oct 2025 15:17:22 +0300 Subject: [PATCH 35/42] CI fix --- .github/workflows/rc-release.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rc-release.yml b/.github/workflows/rc-release.yml index bed71185..9025bd4f 100644 --- a/.github/workflows/rc-release.yml +++ b/.github/workflows/rc-release.yml @@ -398,8 +398,10 @@ jobs: set +e # Don't exit on errors VERSION="${{ needs.validate-release.outputs.version }}" - # Use full version with -rc suffix and 'v' prefix (matches your Jira convention) - JIRA_FIX_VERSION="Flutter SDK v$VERSION" + # Remove -rc suffix for Jira lookup (e.g., 99.99.99-rc1 → 99.99.99) + BASE_VERSION=$(echo "$VERSION" | sed 's/-rc[0-9]*$//') + # Use base version with 'v' prefix (matches your Jira convention) + JIRA_FIX_VERSION="Flutter SDK v$BASE_VERSION" echo "🔍 Looking for Jira tickets with fix version: $JIRA_FIX_VERSION" From 0b3d0798a6ca797cd447e2e8d6542481061dea96 Mon Sep 17 00:00:00 2001 From: Dani-Koza-AF Date: Wed, 15 Oct 2025 15:21:47 +0300 Subject: [PATCH 36/42] yet another fix to CI --- .github/workflows/production-release.yml | 13 ++++++++++--- .github/workflows/rc-release.yml | 13 ++++++++++--- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/.github/workflows/production-release.yml b/.github/workflows/production-release.yml index 0d6a54c6..7005012a 100644 --- a/.github/workflows/production-release.yml +++ b/.github/workflows/production-release.yml @@ -405,10 +405,17 @@ jobs: IOS_SDK_VERSION=$(grep "s.ios.dependency 'AppsFlyerFramework'" ios/appsflyer_sdk.podspec | sed -n "s/.*AppsFlyerFramework',.*'\([^']*\)'.*/\1/p" | head -1) echo "ios_sdk=$IOS_SDK_VERSION" >> $GITHUB_OUTPUT - # Extract Purchase Connector versions from README (fallback to default if not found) - ANDROID_PC_VERSION=$(grep -A 1 "### Purchase Connector versions" README.md | grep "Android" | sed -n 's/.*Android \([0-9.]*\).*/\1/p' || echo "N/A") - IOS_PC_VERSION=$(grep -A 2 "### Purchase Connector versions" README.md | grep "iOS" | sed -n 's/.*iOS \([0-9.]*\).*/\1/p' || echo "N/A") + # Extract Purchase Connector versions from build files + ANDROID_PC_VERSION=$(grep "implementation 'com.appsflyer:purchase-connector:" android/build.gradle | sed -n "s/.*purchase-connector:\([^']*\).*/\1/p" | head -1) + if [ -z "$ANDROID_PC_VERSION" ]; then + ANDROID_PC_VERSION="N/A" + fi echo "android_pc=$ANDROID_PC_VERSION" >> $GITHUB_OUTPUT + + IOS_PC_VERSION=$(grep "s.ios.dependency 'PurchaseConnector'" ios/appsflyer_sdk.podspec | sed -n "s/.*PurchaseConnector',.*'\([^']*\)'.*/\1/p" | head -1) + if [ -z "$IOS_PC_VERSION" ]; then + IOS_PC_VERSION="N/A" + fi echo "ios_pc=$IOS_PC_VERSION" >> $GITHUB_OUTPUT # Extract changelog for this version diff --git a/.github/workflows/rc-release.yml b/.github/workflows/rc-release.yml index 9025bd4f..0f9d1c43 100644 --- a/.github/workflows/rc-release.yml +++ b/.github/workflows/rc-release.yml @@ -371,10 +371,17 @@ jobs: IOS_SDK_VERSION=$(grep "s.ios.dependency 'AppsFlyerFramework'" ios/appsflyer_sdk.podspec | sed -n "s/.*AppsFlyerFramework',.*'\([^']*\)'.*/\1/p" | head -1) echo "ios_sdk=$IOS_SDK_VERSION" >> $GITHUB_OUTPUT - # Extract Purchase Connector versions - ANDROID_PC_VERSION=$(grep -A 1 "### Purchase Connector versions" README.md | grep "Android" | sed -n 's/.*Android \([0-9.]*\).*/\1/p' || echo "N/A") - IOS_PC_VERSION=$(grep -A 2 "### Purchase Connector versions" README.md | grep "iOS" | sed -n 's/.*iOS \([0-9.]*\).*/\1/p' || echo "N/A") + # Extract Purchase Connector versions from build files + ANDROID_PC_VERSION=$(grep "implementation 'com.appsflyer:purchase-connector:" android/build.gradle | sed -n "s/.*purchase-connector:\([^']*\).*/\1/p" | head -1) + if [ -z "$ANDROID_PC_VERSION" ]; then + ANDROID_PC_VERSION="N/A" + fi echo "android_pc=$ANDROID_PC_VERSION" >> $GITHUB_OUTPUT + + IOS_PC_VERSION=$(grep "s.ios.dependency 'PurchaseConnector'" ios/appsflyer_sdk.podspec | sed -n "s/.*PurchaseConnector',.*'\([^']*\)'.*/\1/p" | head -1) + if [ -z "$IOS_PC_VERSION" ]; then + IOS_PC_VERSION="N/A" + fi echo "ios_pc=$IOS_PC_VERSION" >> $GITHUB_OUTPUT # Extract changelog for this version (use base version without -rc suffix) From b720c7857a15ee355c4013bc13736d813c7bcb59 Mon Sep 17 00:00:00 2001 From: Dani-Koza-AF Date: Wed, 15 Oct 2025 15:39:02 +0300 Subject: [PATCH 37/42] Hope it's the last CI fix --- .github/workflows/production-release.yml | 25 +++++++++++++++++------- .github/workflows/rc-release.yml | 23 +++++++++++++++++----- 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/.github/workflows/production-release.yml b/.github/workflows/production-release.yml index 7005012a..3889f48f 100644 --- a/.github/workflows/production-release.yml +++ b/.github/workflows/production-release.yml @@ -458,20 +458,31 @@ jobs: # Fetch tickets from Jira with this fix version JIRA_DOMAIN="${{ secrets.CI_JIRA_DOMAIN || 'appsflyer.atlassian.net' }}" - # Create auth header - AUTH=$(echo -n "${{ secrets.CI_JIRA_EMAIL }}:${{ secrets.CI_JIRA_TOKEN }}" | base64) - - # Query Jira API with error handling - RESPONSE=$(curl -s -w "\n%{http_code}" -X GET \ - -H "Authorization: Basic $AUTH" \ + # URL-encode the JQL query properly + JQL_QUERY="fixVersion=\"${JIRA_FIX_VERSION}\"" + # Use jq for proper URL encoding + ENCODED_JQL=$(echo "$JQL_QUERY" | jq -sRr @uri) + + echo "📡 Querying Jira API..." + echo "Domain: $JIRA_DOMAIN" + echo "JQL: $JQL_QUERY" + + # Query Jira API with error handling and verbose output + RESPONSE=$(curl -s -w "\n%{http_code}" \ + -u "${{ secrets.CI_JIRA_EMAIL }}:${{ secrets.CI_JIRA_TOKEN }}" \ + -H "Accept: application/json" \ -H "Content-Type: application/json" \ - "https://${JIRA_DOMAIN}/rest/api/3/search?jql=fixVersion=\"${JIRA_FIX_VERSION}\"&fields=key,summary&maxResults=20") + "https://${JIRA_DOMAIN}/rest/api/3/search?jql=${ENCODED_JQL}&fields=key,summary&maxResults=20") HTTP_CODE=$(echo "$RESPONSE" | tail -n1) BODY=$(echo "$RESPONSE" | sed '$d') + echo "HTTP Status: $HTTP_CODE" + if [[ "$HTTP_CODE" != "200" ]]; then echo "âš ī¸ Jira API request failed with status $HTTP_CODE" + echo "Response body (first 500 chars):" + echo "$BODY" | head -c 500 echo "tickets=No assigned fix version found" >> $GITHUB_OUTPUT exit 0 fi diff --git a/.github/workflows/rc-release.yml b/.github/workflows/rc-release.yml index 0f9d1c43..9b149712 100644 --- a/.github/workflows/rc-release.yml +++ b/.github/workflows/rc-release.yml @@ -420,19 +420,32 @@ jobs: fi JIRA_DOMAIN="${{ secrets.CI_JIRA_DOMAIN || 'appsflyer.atlassian.net' }}" - AUTH=$(echo -n "${{ secrets.CI_JIRA_EMAIL }}:${{ secrets.CI_JIRA_TOKEN }}" | base64) - # Query Jira API with error handling - RESPONSE=$(curl -s -w "\n%{http_code}" -X GET \ - -H "Authorization: Basic $AUTH" \ + # URL-encode the JQL query properly + JQL_QUERY="fixVersion=\"${JIRA_FIX_VERSION}\"" + # Use curl's --data-urlencode for proper encoding + ENCODED_JQL=$(echo "$JQL_QUERY" | jq -sRr @uri) + + echo "📡 Querying Jira API..." + echo "Domain: $JIRA_DOMAIN" + echo "JQL: $JQL_QUERY" + + # Query Jira API with error handling and verbose output + RESPONSE=$(curl -s -w "\n%{http_code}" \ + -u "${{ secrets.CI_JIRA_EMAIL }}:${{ secrets.CI_JIRA_TOKEN }}" \ + -H "Accept: application/json" \ -H "Content-Type: application/json" \ - "https://${JIRA_DOMAIN}/rest/api/3/search?jql=fixVersion=\"${JIRA_FIX_VERSION}\"&fields=key,summary&maxResults=20") + "https://${JIRA_DOMAIN}/rest/api/3/search?jql=${ENCODED_JQL}&fields=key,summary&maxResults=20") HTTP_CODE=$(echo "$RESPONSE" | tail -n1) BODY=$(echo "$RESPONSE" | sed '$d') + echo "HTTP Status: $HTTP_CODE" + if [[ "$HTTP_CODE" != "200" ]]; then echo "âš ī¸ Jira API request failed with status $HTTP_CODE" + echo "Response body (first 500 chars):" + echo "$BODY" | head -c 500 echo "tickets=No assigned fix version found" >> $GITHUB_OUTPUT exit 0 fi From 0e4fa80dad49bb93462f6940df731e20d558bd93 Mon Sep 17 00:00:00 2001 From: Dani-Koza-AF Date: Wed, 15 Oct 2025 15:59:32 +0300 Subject: [PATCH 38/42] fix: update Jira API to use /search/jql endpoint (fixes HTTP 410) --- .github/workflows/production-release.yml | 3 ++- .github/workflows/rc-release.yml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/production-release.yml b/.github/workflows/production-release.yml index 3889f48f..0db7841c 100644 --- a/.github/workflows/production-release.yml +++ b/.github/workflows/production-release.yml @@ -468,11 +468,12 @@ jobs: echo "JQL: $JQL_QUERY" # Query Jira API with error handling and verbose output + # Using the new /search/jql endpoint as per Jira API v3 RESPONSE=$(curl -s -w "\n%{http_code}" \ -u "${{ secrets.CI_JIRA_EMAIL }}:${{ secrets.CI_JIRA_TOKEN }}" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ - "https://${JIRA_DOMAIN}/rest/api/3/search?jql=${ENCODED_JQL}&fields=key,summary&maxResults=20") + "https://${JIRA_DOMAIN}/rest/api/3/search/jql?jql=${ENCODED_JQL}&fields=key,summary&maxResults=20") HTTP_CODE=$(echo "$RESPONSE" | tail -n1) BODY=$(echo "$RESPONSE" | sed '$d') diff --git a/.github/workflows/rc-release.yml b/.github/workflows/rc-release.yml index 9b149712..3ed16869 100644 --- a/.github/workflows/rc-release.yml +++ b/.github/workflows/rc-release.yml @@ -431,11 +431,12 @@ jobs: echo "JQL: $JQL_QUERY" # Query Jira API with error handling and verbose output + # Using the new /search/jql endpoint as per Jira API v3 RESPONSE=$(curl -s -w "\n%{http_code}" \ -u "${{ secrets.CI_JIRA_EMAIL }}:${{ secrets.CI_JIRA_TOKEN }}" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ - "https://${JIRA_DOMAIN}/rest/api/3/search?jql=${ENCODED_JQL}&fields=key,summary&maxResults=20") + "https://${JIRA_DOMAIN}/rest/api/3/search/jql?jql=${ENCODED_JQL}&fields=key,summary&maxResults=20") HTTP_CODE=$(echo "$RESPONSE" | tail -n1) BODY=$(echo "$RESPONSE" | sed '$d') From f968d6483ebf1d97efefb0cec7ab1b693286b45b Mon Sep 17 00:00:00 2001 From: Dani-Koza-AF Date: Tue, 21 Oct 2025 13:20:56 +0300 Subject: [PATCH 39/42] readme update and dart analyze fix fix in example project only --- README.md | 4 +-- example/lib/home_container.dart | 60 ++++++++++++++++----------------- example/lib/main_page.dart | 10 +++--- example/lib/text_border.dart | 6 ++-- 4 files changed, 40 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 05b5997b..453d92f2 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,9 @@ [![pub package](https://img.shields.io/pub/v/appsflyer_sdk.svg)](https://pub.dartlang.org/packages/appsflyer_sdk) ![Coverage](https://raw.githubusercontent.com/AppsFlyerSDK/appsflyer-flutter-plugin/master/coverage_badge.svg) -🛠 In order for us to provide optimal support, we would kindly ask you to submit any issues to +🛠 In order for us to provide optimal support, please contact AppsFlyer support through the Customer Assistant Chatbot for assistance with troubleshooting issues or product guidance.
+To do so, please follow [this article](https://support.appsflyer.com/hc/en-us/articles/23583984402193-Using-the-Customer-Assistant-Chatbot) -> *When submitting an issue please specify your AppsFlyer sign-up (account) email , your app ID , production steps, logs, code snippets and any additional relevant information.* ## SDK Versions diff --git a/example/lib/home_container.dart b/example/lib/home_container.dart index 78c6c93b..8d208be6 100644 --- a/example/lib/home_container.dart +++ b/example/lib/home_container.dart @@ -59,7 +59,7 @@ class _HomeContainerState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( - padding: EdgeInsets.all(20.0), + padding: const EdgeInsets.all(20.0), decoration: BoxDecoration( color: Colors.white, border: Border.all(color: Colors.blueGrey, width: 0.5), @@ -67,7 +67,7 @@ class _HomeContainerState extends State { ), child: Column( children: [ - Text( + const Text( "APPSFLYER SDK", style: TextStyle( fontSize: 18, @@ -75,7 +75,7 @@ class _HomeContainerState extends State { fontWeight: FontWeight.w500, ), ), - SizedBox(height: AppConstants.topPadding), + const SizedBox(height: AppConstants.topPadding), TextBorder( controller: TextEditingController( text: widget.onData.isNotEmpty @@ -84,7 +84,7 @@ class _HomeContainerState extends State { ), labelText: "CONVERSION DATA", ), - SizedBox(height: 12.0), + const SizedBox(height: 12.0), TextBorder( controller: TextEditingController( text: widget.deepLinkData != null @@ -96,9 +96,9 @@ class _HomeContainerState extends State { ], ), ), - SizedBox(height: 12.0), + const SizedBox(height: 12.0), Container( - padding: EdgeInsets.all(20.0), + padding: const EdgeInsets.all(20.0), decoration: BoxDecoration( color: Colors.white, border: Border.all(color: Colors.blueGrey, width: 0.5), @@ -106,7 +106,7 @@ class _HomeContainerState extends State { ), child: Column( children: [ - Text( + const Text( "EVENT LOGGER", style: TextStyle( fontSize: 18, @@ -114,64 +114,64 @@ class _HomeContainerState extends State { fontWeight: FontWeight.w500, ), ), - SizedBox(height: 12.0), + const SizedBox(height: 12.0), TextBorder( controller: TextEditingController( text: "Event Name: $eventName\nEvent Values: $eventValues"), labelText: "EVENT REQUEST", ), - SizedBox(height: 12.0), + const SizedBox(height: 12.0), TextBorder( labelText: "SERVER RESPONSE", controller: TextEditingController(text: _logEventResponse), ), - SizedBox(height: 20.0), + const SizedBox(height: 20.0), ElevatedButton( onPressed: () { widget.logEvent(eventName, eventValues).then((onValue) { setState(() { _logEventResponse = - "Event Status: " + onValue.toString(); + "Event Status: $onValue"; }); }).catchError((onError) { setState(() { - _logEventResponse = "Error: " + onError.toString(); + _logEventResponse = "Error: $onError"; }); }); }, - child: Text("Trigger Purchase Event"), style: ElevatedButton.styleFrom( backgroundColor: Colors.white, padding: - EdgeInsets.symmetric(horizontal: 20, vertical: 10), - textStyle: TextStyle( + const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + textStyle: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ), + child: Text("Trigger Purchase Event"), ), ElevatedButton( onPressed: () { widget.logAdRevenueEvent(); }, - child: Text("Trigger AdRevenue Event"), style: ElevatedButton.styleFrom( backgroundColor: Colors.white, padding: - EdgeInsets.symmetric(horizontal: 20, vertical: 10), - textStyle: TextStyle( + const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + textStyle: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ), + child: Text("Trigger AdRevenue Event"), ), ], ), ), - SizedBox(height: 12.0), + const SizedBox(height: 12.0), Container( - padding: EdgeInsets.all(20.0), + padding: const EdgeInsets.all(20.0), decoration: BoxDecoration( color: Colors.white, border: Border.all(color: Colors.blueGrey, width: 0.5), @@ -179,7 +179,7 @@ class _HomeContainerState extends State { ), child: Column( children: [ - Text( + const Text( "PURCHASE VALIDATION V2", style: TextStyle( fontSize: 18, @@ -187,10 +187,10 @@ class _HomeContainerState extends State { fontWeight: FontWeight.w500, ), ), - SizedBox(height: 12.0), + const SizedBox(height: 12.0), TextFormField( controller: _purchaseTokenController, - decoration: InputDecoration( + decoration: const InputDecoration( labelText: "Purchase Token", border: OutlineInputBorder(), contentPadding: @@ -198,23 +198,23 @@ class _HomeContainerState extends State { ), maxLines: 2, ), - SizedBox(height: 12.0), + const SizedBox(height: 12.0), TextFormField( controller: _productIdController, - decoration: InputDecoration( + decoration: const InputDecoration( labelText: "Product ID", border: OutlineInputBorder(), contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), ), ), - SizedBox(height: 12.0), + const SizedBox(height: 12.0), TextBorder( labelText: "VALIDATION RESPONSE", controller: TextEditingController(text: _validationResponse), ), - SizedBox(height: 20.0), + const SizedBox(height: 20.0), ElevatedButton( onPressed: () { final purchaseToken = @@ -243,17 +243,17 @@ class _HomeContainerState extends State { }); }); }, - child: Text("Validate Purchase V2"), style: ElevatedButton.styleFrom( backgroundColor: Colors.green, foregroundColor: Colors.white, padding: - EdgeInsets.symmetric(horizontal: 20, vertical: 10), - textStyle: TextStyle( + const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + textStyle: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ), + child: Text("Validate Purchase V2"), ), ], ), diff --git a/example/lib/main_page.dart b/example/lib/main_page.dart index 7e66d77b..a3fd1130 100644 --- a/example/lib/main_page.dart +++ b/example/lib/main_page.dart @@ -64,7 +64,7 @@ class MainPageState extends State { // Conversion data callback _appsflyerSdk.onInstallConversionData((res) { - print("onInstallConversionData res: " + res.toString()); + print("onInstallConversionData res: $res"); setState(() { _gcd = res; }); @@ -72,7 +72,7 @@ class MainPageState extends State { // App open attribution callback _appsflyerSdk.onAppOpenAttribution((res) { - print("onAppOpenAttribution res: " + res.toString()); + print("onAppOpenAttribution res: $res"); setState(() { _deepLinkData = res; }); @@ -95,7 +95,7 @@ class MainPageState extends State { print("deep link status parsing error"); break; } - print("onDeepLinking res: " + dp.toString()); + print("onDeepLinking res: $dp"); setState(() { _deepLinkData = dp.toJson(); }); @@ -112,7 +112,7 @@ class MainPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text('AppsFlyer SDK example app'), + title: const Text('AppsFlyer SDK example app'), centerTitle: true, backgroundColor: Colors.green, ), @@ -142,7 +142,7 @@ class MainPageState extends State { }, ); }, - child: Text("START SDK"), + child: const Text("START SDK"), ) ], ), diff --git a/example/lib/text_border.dart b/example/lib/text_border.dart index cdcb9130..cf0c23c6 100644 --- a/example/lib/text_border.dart +++ b/example/lib/text_border.dart @@ -20,12 +20,12 @@ class TextBorder extends StatelessWidget { maxLines: null, decoration: InputDecoration( labelText: labelText, - labelStyle: TextStyle( + labelStyle: const TextStyle( color: Colors.blueGrey), // Change the color of the label - border: OutlineInputBorder( + border: const OutlineInputBorder( borderSide: BorderSide(width: 1.0), ), - focusedBorder: OutlineInputBorder( + focusedBorder: const OutlineInputBorder( borderSide: BorderSide(width: 1.0), ), ), From 9455db35349cef1b6f6c34dbf2ee8414f40cbc61 Mon Sep 17 00:00:00 2001 From: Dani-Koza-AF Date: Tue, 21 Oct 2025 15:13:50 +0300 Subject: [PATCH 40/42] verions bumps + modernized to flutter lint --- CHANGELOG.md | 6 ++++-- README.md | 4 ++-- analysis_options.yaml | 3 ++- example/lib/home_container.dart | 15 +++++++-------- ios/appsflyer_sdk.podspec | 6 +++--- lib/src/appsflyer_constants.dart | 2 +- pubspec.yaml | 4 ++-- 7 files changed, 21 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f9542a7..a363bdb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,13 @@ # Versions -## 6.17.6 +## 6.17.7 -- Updated to AppsFlyer SDK v6.17.6 for iOS and Flutter plugin version +- Updated to AppsFlyer SDK v6.17.7 for iOS and Flutter plugin version - Android AppsFlyer SDK remains at v6.17.3 +- iOS AppsFlyer SDK updgraded to v6.17.7 - Documents update - Purchase Connector module updated (to support Google Billing Library 8) +- Code cleanups ## 6.17.5 diff --git a/README.md b/README.md index 453d92f2..eddca451 100644 --- a/README.md +++ b/README.md @@ -12,12 +12,12 @@ To do so, please follow [this article](https://support.appsflyer.com/hc/en-us/ar ## SDK Versions - Android AppsFlyer SDK **v6.17.3** -- iOS AppsFlyer SDK **v6.17.6** +- iOS AppsFlyer SDK **v6.17.7** ### Purchase Connector versions - Android 2.2.0 -- iOS 6.17.6 +- iOS 6.17.7 ## ❗❗ Breaking changes when updating to v6.x.x❗❗ diff --git a/analysis_options.yaml b/analysis_options.yaml index aff66236..732a61cd 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,4 +1,5 @@ -include: package:effective_dart/analysis_options.yaml +include: package:flutter_lints/flutter.yaml + linter: rules: public_member_api_docs: false diff --git a/example/lib/home_container.dart b/example/lib/home_container.dart index 8d208be6..e3e53761 100644 --- a/example/lib/home_container.dart +++ b/example/lib/home_container.dart @@ -131,8 +131,7 @@ class _HomeContainerState extends State { onPressed: () { widget.logEvent(eventName, eventValues).then((onValue) { setState(() { - _logEventResponse = - "Event Status: $onValue"; + _logEventResponse = "Event Status: $onValue"; }); }).catchError((onError) { setState(() { @@ -142,8 +141,8 @@ class _HomeContainerState extends State { }, style: ElevatedButton.styleFrom( backgroundColor: Colors.white, - padding: - const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 10), textStyle: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, @@ -157,8 +156,8 @@ class _HomeContainerState extends State { }, style: ElevatedButton.styleFrom( backgroundColor: Colors.white, - padding: - const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 10), textStyle: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, @@ -246,8 +245,8 @@ class _HomeContainerState extends State { style: ElevatedButton.styleFrom( backgroundColor: Colors.green, foregroundColor: Colors.white, - padding: - const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 10), textStyle: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, diff --git a/ios/appsflyer_sdk.podspec b/ios/appsflyer_sdk.podspec index 5e4ef82d..dcca906a 100644 --- a/ios/appsflyer_sdk.podspec +++ b/ios/appsflyer_sdk.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'appsflyer_sdk' - s.version = '6.17.6' + s.version = '6.17.7' s.summary = 'AppsFlyer Integration for Flutter' s.description = 'AppsFlyer is the market leader in mobile advertising attribution & analytics, helping marketers to pinpoint their targeting, optimize their ad spend and boost their ROI.' s.homepage = 'https://github.com/AppsFlyerSDK/flutter_appsflyer_sdk' @@ -21,12 +21,12 @@ Pod::Spec.new do |s| ss.source_files = 'Classes/**/*' ss.public_header_files = 'Classes/**/*.h' ss.dependency 'Flutter' - ss.ios.dependency 'AppsFlyerFramework','6.17.6' + ss.ios.dependency 'AppsFlyerFramework','6.17.7' end s.subspec 'PurchaseConnector' do |ss| ss.dependency 'Flutter' - ss.ios.dependency 'PurchaseConnector', '6.17.6' + ss.ios.dependency 'PurchaseConnector', '6.17.7' ss.source_files = 'PurchaseConnector/**/*' ss.public_header_files = 'PurchaseConnector/**/*.h' diff --git a/lib/src/appsflyer_constants.dart b/lib/src/appsflyer_constants.dart index b27b16f7..7dbe6a40 100644 --- a/lib/src/appsflyer_constants.dart +++ b/lib/src/appsflyer_constants.dart @@ -3,7 +3,7 @@ part of appsflyer_sdk; enum EmailCryptType { EmailCryptTypeNone, EmailCryptTypeSHA256 } class AppsflyerConstants { - static const String PLUGIN_VERSION = "6.17.6"; + static const String PLUGIN_VERSION = "6.17.7"; static const String AF_DEV_KEY = "afDevKey"; static const String AF_APP_Id = "afAppId"; static const String AF_IS_DEBUG = "isDebug"; diff --git a/pubspec.yaml b/pubspec.yaml index c9fdc9f2..f4c5aeca 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: appsflyer_sdk description: A Flutter plugin for AppsFlyer SDK. Supports iOS and Android. -version: 6.17.6 +version: 6.17.7 homepage: https://github.com/AppsFlyerSDK/flutter_appsflyer_sdk @@ -18,7 +18,7 @@ dev_dependencies: sdk: flutter test: ^1.16.5 mockito: ^5.4.4 - effective_dart: ^1.3.0 + flutter_lints: ^5.0.0 build_runner: ^2.3.0 json_serializable: ^6.5.4 From 9a41db18110464299598fb8f1b3f4b251d5f747a Mon Sep 17 00:00:00 2001 From: Dani-Koza-AF Date: Tue, 21 Oct 2025 15:44:39 +0300 Subject: [PATCH 41/42] Documentation Updates - regarding push notifications --- CHANGELOG.md | 13 +++- doc/API.md | 175 +++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 167 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a363bdb5..5a96323a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,17 @@ - Updated to AppsFlyer SDK v6.17.7 for iOS and Flutter plugin version - Android AppsFlyer SDK remains at v6.17.3 -- iOS AppsFlyer SDK updgraded to v6.17.7 -- Documents update -- Purchase Connector module updated (to support Google Billing Library 8) +- iOS AppsFlyer SDK upgraded to v6.17.7 +- Android Purchase Connector module updated (to support Google Billing Library 8) - Code cleanups +- **Documentation Updates:** + - Enhanced push notification measurement documentation with clear separation between traditional `af` object approach and OneLink URL approach + - Added comprehensive iOS-specific requirements for OneLink push notification integration + - Clarified the need to call `sendPushNotificationData()` on iOS when using `addPushNotificationDeepLinkPath()` + - Added complete code examples for both push notification integration approaches with Firebase Cloud Messaging + - Added Flutter 3.27+ breaking change documentation for deep linking (must disable Flutter's built-in deep linking to avoid conflicts with AppsFlyer) + - Replaced `effective_dart` with `flutter_lints` in development dependencies + ## 6.17.5 diff --git a/doc/API.md b/doc/API.md index a57323cd..4717ae32 100644 --- a/doc/API.md +++ b/doc/API.md @@ -687,27 +687,37 @@ try { --- ## **
`void sendPushNotificationData(Map? userInfo)`** -Push-notification campaigns are used to create re-engagements with existing users -> [Learn more here](https://support.appsflyer.com/hc/en-us/articles/207364076-Measuring-Push-Notification-Re-Engagement-Campaigns) +Push-notification campaigns are used to create re-engagements with existing users → [Learn more here](https://support.appsflyer.com/hc/en-us/articles/207364076-Measuring-Push-Notification-Re-Engagement-Campaigns) -🟩 **Android:**
-The AppsFlyer SDK **requires a** **valid Activity context** to process the push payload. -**Do NOT call this method from the background isolate** (e.g., _firebaseMessagingBackgroundHandler), as the activity is not yet created. +### Platform-Specific Requirements + +🟩 **Android:** +The AppsFlyer SDK **requires a valid Activity context** to process the push payload. +**Do NOT call this method from the background isolate** (e.g., `_firebaseMessagingBackgroundHandler`), as the activity is not yet created. Instead, **delay calling this method** until the Flutter app is fully resumed and the activity is alive. -🍎 **iOS:**
+🍎 **iOS:** This method can be safely called at any point during app launch or when receiving a push notification. +--- + +## Integration Approaches + +AppsFlyer supports two approaches for measuring push notification campaigns: + +### Approach 1: Traditional Attribution Parameters (`af` object) -_**Usage example with Firebase Cloud Messaging:**_
-Given the fact that push message data contains custom key called `af` that contains the attribution data you want to send to AppsFlyer in JSON format. The following attribution parameters are required: `pid`, `is_retargeting`, `c`. +Use this approach when your push payload contains a custom `af` object with attribution parameters. -đŸ“Ļ **Example Push Message Payload** +**Required parameters:** `pid`, `is_retargeting`, `c` + +đŸ“Ļ **Example Push Payload with `af` Object:** ```json { - "af": { + "af": { "c": "test_campaign", "is_retargeting": true, - "pid": "push_provider_int", + "pid": "push_provider_int" }, "aps": { "alert": "Get 5000 Coins", @@ -717,38 +727,167 @@ Given the fact that push message data contains custom key called `af` that conta } ``` -1ī¸âƒŖ Handle Foreground Messages +**Implementation (Android & iOS):** + ```dart +// 1ī¸âƒŖ Handle Foreground Messages FirebaseMessaging.onMessage.listen((RemoteMessage message) { appsFlyerSdk.sendPushNotificationData(message.data); }); -``` -2ī¸âƒŖ Handle Notification Taps (App in Background) -```dart + +// 2ī¸âƒŖ Handle Notification Taps (App in Background) FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) { appsFlyerSdk.sendPushNotificationData(message.data); }); + +// 3ī¸âƒŖ Handle App Launch from Push (Terminated State) +// Store payload in background handler, then pass to AppsFlyer when app resumes +Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('pending_af_push', jsonEncode(message.data)); +} + +// In your main() or splash screen after Flutter is initialized: +void handlePendingPush() async { + final prefs = await SharedPreferences.getInstance(); + final json = prefs.getString('pending_af_push'); + if (json != null) { + final payload = jsonDecode(json); + appsFlyerSdk.sendPushNotificationData(payload); + await prefs.remove('pending_af_push'); + } +} ``` -3ī¸âƒŖ Handle App Launch from Push (Terminated State) -Store the payload using `_firebaseMessagingBackgroundHandler`, then pass it to AppsFlyer once the app is resumed. + +Call `handlePendingPush()` during app startup (e.g., in your `main()` or inside your splash screen after ensuring Flutter is initialized). + +--- + +### Approach 2: OneLink URL in Push Payload (Recommended) + +Use this approach when your push payload contains a **OneLink URL** for deep linking. This method provides a unified deep linking experience. + +> âš ī¸ **Important:** This approach requires calling **two different methods** depending on the platform! + +#### **Step 1: Configure Deep Link Path (BOTH Platforms)** + +Call `addPushNotificationDeepLinkPath` **BEFORE** initializing the SDK to tell AppsFlyer where to find the OneLink URL in your push payload. + ```dart +// Must be called BEFORE initSdk() or startSDK() +appsFlyerSdk.addPushNotificationDeepLinkPath(["deeply", "nested", "deep_link"]); + +// Then initialize the SDK +await appsFlyerSdk.initSdk( + registerOnDeepLinkingCallback: true // Enable deep linking callback +); +``` + +#### **Step 2: Send Push Payload to SDK** + +**🟩 Android:** +On Android, calling `addPushNotificationDeepLinkPath` is **sufficient**. The SDK automatically extracts and processes the OneLink URL. + +**🍎 iOS:** +On iOS, you **MUST also call** `sendPushNotificationData(userInfo)` to pass the push payload to the SDK. The SDK then internally calls `handlePushNotification` to extract and process the OneLink URL. + +đŸ“Ļ **Example Push Payload with OneLink URL:** +```json +{ + "deeply": { + "nested": { + "deep_link": "https://yourapp.onelink.me/ABC/campaign123" + } + }, + "aps": { + "alert": "Check out our new feature!", + "badge": "1", + "sound": "default" + } +} +``` + +**Complete Implementation Example:** + +```dart +// ======================================== +// 1. Configure SDK (in main.dart or app initialization) +// ======================================== +void initializeAppsFlyer() async { + // STEP 1: Configure the deep link path BEFORE starting SDK + appsFlyerSdk.addPushNotificationDeepLinkPath(["deeply", "nested", "deep_link"]); + + // STEP 2: Initialize SDK with deep linking callback + await appsFlyerSdk.initSdk( + registerOnDeepLinkingCallback: true + ); + + // STEP 3: Set up deep linking callback to handle the OneLink URL + appsFlyerSdk.onDeepLinking((DeepLinkResult result) { + if (result.status == Status.FOUND) { + print("Deep link found: ${result.deepLink?.deepLinkValue}"); + // Handle deep link navigation here + } + }); +} + +// ======================================== +// 2. Handle Push Notifications +// ======================================== + +// 🍎 iOS: MUST call sendPushNotificationData +// 🟩 Android: Optional (SDK auto-handles), but recommended for consistency + +// 1ī¸âƒŖ Foreground Messages +FirebaseMessaging.onMessage.listen((RemoteMessage message) { + // iOS: Required to process OneLink URL + // Android: SDK processes automatically, but calling doesn't hurt + appsFlyerSdk.sendPushNotificationData(message.data); +}); + +// 2ī¸âƒŖ Background Notification Taps (App in Background) +FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) { + // iOS: Required to process OneLink URL + appsFlyerSdk.sendPushNotificationData(message.data); +}); + +// 3ī¸âƒŖ App Launch from Push (Terminated State) Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { final prefs = await SharedPreferences.getInstance(); await prefs.setString('pending_af_push', jsonEncode(message.data)); } -// In your main() or splash screen after Flutter is initialized: +// In main() or splash screen: void handlePendingPush() async { final prefs = await SharedPreferences.getInstance(); final json = prefs.getString('pending_af_push'); if (json != null) { final payload = jsonDecode(json); + // iOS: Required to process OneLink URL from terminated state appsFlyerSdk.sendPushNotificationData(payload); await prefs.remove('pending_af_push'); } } ``` -Call handlePendingPush() during app startup (e.g., in your main() or inside your splash screen after ensuring Flutter is initialized). + +#### **Key Differences Between Approaches:** + +|| Traditional `af` Object | OneLink URL (Recommended) | +|---|---|---| +| **Android** | `sendPushNotificationData(data)` | `addPushNotificationDeepLinkPath()` (auto-handles) | +| **iOS** | `sendPushNotificationData(data)` | `addPushNotificationDeepLinkPath()` **+** `sendPushNotificationData(data)` | +| **Deep Linking** | Basic attribution only | Full deep linking with `onDeepLinking` callback | +| **Use Case** | Simple re-engagement | Re-engagement + in-app navigation | + +--- + +### Summary + +- **Traditional approach**: Always call `sendPushNotificationData(payload)` on both platforms +- **OneLink approach (Recommended)**: + - ✅ **Both platforms**: Call `addPushNotificationDeepLinkPath()` before SDK init + - ✅ **iOS only**: Also call `sendPushNotificationData(payload)` when push is received + - ✅ **Both platforms**: Handle deep links in `onDeepLinking` callback --- From 9f09927697e5220211271cd3b9b5fffe8cfba271 Mon Sep 17 00:00:00 2001 From: Dani-Koza-AF Date: Wed, 22 Oct 2025 14:21:43 +0300 Subject: [PATCH 42/42] Added GoogleAdsOnDeviceConversion dependency for testing ODM --- example/ios/Podfile | 4 ++++ example/ios/Runner.xcodeproj/project.pbxproj | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/example/ios/Podfile b/example/ios/Podfile index 9a3af86b..a4220ff8 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -32,6 +32,10 @@ target 'Runner' do use_modular_headers! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + + # Google Ads On-Device Conversion SDK for testing AppsFlyer integration + pod 'GoogleAdsOnDeviceConversion', '~> 3.2.0' + target 'RunnerTests' do inherit! :search_paths end diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index bbde4090..b194b797 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -200,6 +200,7 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, D2F636E2006BF2310FF8EF27 /* [CP] Copy Pods Resources */, + CBA4E0AD93E266A8CFF44591 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -330,6 +331,23 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + CBA4E0AD93E266A8CFF44591 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; D2F636E2006BF2310FF8EF27 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647;