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
- Android Purchase Connector
- iOS Purchase Connector
🛠 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.
- Important Note
- Adding The Connector To Your Project
- Basic Integration Of The Connector
- StoreKit Version Configuration (iOS)
- Register Validation Results Listeners
- Testing the Integration
- ProGuard Rules for Android
- Full Code Example
🚨 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 (Minimum required version: 8.x.x and higher).
- For iOS, it depends on 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 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.
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.
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.
$AppsFlyerPurchaseConnector = trueFor Android, in your gradle.properties file located within the Android folder of your Flutter project,, set appsflyer.enable_purchase_connector to true.
appsflyer.enable_purchase_connector=trueOnce you set these properties, the Purchase Validation feature will be integrated into your project and you can utilize its functionality in your app.
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.
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 totrue, the connector logs all subscription events.logInApps: If set totrue, the connector logs all in-app purchase events.sandbox: If set totrue, transactions are tested in a sandbox environment. Be sure to set this tofalsein production.storeKitVersion: (iOS only) Specifies which StoreKit version to use. Defaults toStoreKitVersion.storeKit1if not specified.
Here's an example usage:
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
storeKitVersion: StoreKitVersion.storeKit1, // iOS only: StoreKit version (defaults to storeKit1)
),
);
// 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:
void main() {
// Correct usage: Providing configuration at first instantiation
final purchaseConnector1 = PurchaseConnector(
config: PurchaseConnectorConfiguration(
logSubscriptions: true,
logInApps: true,
sandbox: true,
storeKitVersion: StoreKitVersion.storeKit1, // Default StoreKit version
),
);
// 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,
storeKitVersion: StoreKitVersion.storeKit2, // This will be ignored
),
);
// 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 the SDK instance to observe transactions.
This should be called right after calling the
AppsflyerSdkstart. CallingstartObservingTransactionsactivates 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.
// start
afPurchaseClient.startObservingTransactions();Stop the SDK instance from observing transactions.
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
stopAPI
// start
afPurchaseClient.stopObservingTransactions();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.
final afPurchaseClient = PurchaseConnector(
config: PurchaseConnectorConfiguration(logSubscriptions: true));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.
final afPurchaseClient = PurchaseConnector(
config: PurchaseConnectorConfiguration(logInApps: true));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.
StoreKitVersion.storeKit1(Default) - Uses the original StoreKit frameworkStoreKitVersion.storeKit2- Uses the modern StoreKit 2 framework (iOS 15.0+)
Using StoreKit 1 (Default):
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:
final afPurchaseClient = PurchaseConnector(
config: PurchaseConnectorConfiguration(
logSubscriptions: true,
logInApps: true,
sandbox: true,
storeKitVersion: StoreKitVersion.storeKit1, // Explicitly set to StoreKit 1
),
);Using StoreKit 2:
final afPurchaseClient = PurchaseConnector(
config: PurchaseConnectorConfiguration(
logSubscriptions: true,
logInApps: true,
sandbox: true,
storeKitVersion: StoreKitVersion.storeKit2, // Use modern StoreKit 2
),
);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:
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 toStoreKitVersion.storeKit1for 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.
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.
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.
| 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. |
// set listeners for Android
afPurchaseClient.setSubscriptionValidationResultListener(
(Map<String, SubscriptionValidationResult>? result) {
// handle subscription validation result for Android
}, (String result, JVMThrowable? error) {
// handle subscription validation error for Android
});afPurchaseClient.setInAppValidationResultListener(
(Map<String, InAppPurchaseValidationResult>? result) {
// handle in-app validation result for Android
}, (String result, JVMThrowable? error) {
// handle in-app validation error for Android
});afPurchaseClient.setDidReceivePurchaseRevenueValidationInfo((validationInfo, error) {
// handle subscription and in-app validation result and errors for iOS
});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.
For Android, testing your integration with the Google Play Billing Library 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.
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
// Example configuration for testing with StoreKit 2
final purchaseConnector = PurchaseConnector(
config: PurchaseConnectorConfiguration(
sandbox: true, // Enable sandbox for testing
storeKitVersion: StoreKitVersion.storeKit2,
logSubscriptions: true,
logInApps: true,
),
);*IMPORTANT NOTE: Before releasing your app to production please be sure to set
sandboxtofalse. If a production purchase event is sent in sandbox mode, your event will not be validated properly! *
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:
// Testing in a sandbox environment with StoreKit 1 (default)
final purchaseConnector = PurchaseConnector(
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
)
);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.
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:
-keep class com.appsflyer.** { *; }
-keep class kotlin.jvm.internal.Intrinsics{ *; }
-keep class kotlin.collections.**{ *; }PurchaseConnectorConfiguration config = PurchaseConnectorConfiguration(
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
afPurchaseClient.setSubscriptionValidationResultListener(
(Map<String, SubscriptionValidationResult>? 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<String, InAppPurchaseValidationResult>? 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();