diff --git a/.github/workflows/build-apps-workflow.yml b/.github/workflows/build-apps-workflow.yml
index 8e88e8a9..01eb0270 100644
--- a/.github/workflows/build-apps-workflow.yml
+++ b/.github/workflows/build-apps-workflow.yml
@@ -5,20 +5,27 @@ on:
jobs:
Build-RN-android:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v3
- - name: install react-native-appsflyer on an Android app
- run: |
- cd demos/appsflyer-react-native-app
- yarn install
- yarn add ../../ --save
- - name: Build apk
- run: |
- cd demos/appsflyer-react-native-app/android
- chmod +x ./gradlew
- ./gradlew assembleRelease
-
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v2
+ with:
+ java-version: '17'
+ distribution: 'adopt'
+
+ - name: install react-native-appsflyer on an Android app
+ run: |
+ cd demos/appsflyer-react-native-app
+ yarn install
+ yarn add ../../ --save
+
+ - name: Build apk
+ run: |
+ cd demos/appsflyer-react-native-app/android
+ chmod +x ./gradlew
+ ./gradlew assembleRelease
# Build-RN-ios:
# runs-on: macos-latest
# steps:
diff --git a/.github/workflows/responseToSupportIssue.yml b/.github/workflows/responseToSupportIssue.yml
index cb67a697..23e3e648 100644
--- a/.github/workflows/responseToSupportIssue.yml
+++ b/.github/workflows/responseToSupportIssue.yml
@@ -19,11 +19,5 @@ jobs:
issue-number: ${{ github.event.issue.number }}
body: |
π Hi @${{ github.event.issue.user.login }} and Thank you for reaching out to us.
- In order for us to provide optimal support, please submit a ticket to our support team at support@appsflyer.com.
- When submitting the ticket, please specify:
- - β
your AppsFlyer sign-up (account) email
- - β
app ID
- - β
production steps
- - β
logs
- - β
code snippets
- - β
and any additional relevant information.
+ You can 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).
diff --git a/.github/workflows/scripts/updateReadme.sh b/.github/workflows/scripts/updateReadme.sh
index d2a5dc32..23c706e3 100755
--- a/.github/workflows/scripts/updateReadme.sh
+++ b/.github/workflows/scripts/updateReadme.sh
@@ -1,6 +1,6 @@
#!/bin/bash
-ios_sdk_version=$(cat react-native-appsflyer.podspec | grep '\'AppsFlyerFramework\' | grep -Eo '[0-9].[0-9]+.[0-9]+')
-android_sdk_version=$(cat android/build.gradle | grep 'com.appsflyer:af-android-sdk' | grep -Eo '[0-9].[0-9]+.[0-9]+')
-sed -i -e -r "s/Android AppsFlyer SDK \*\*v[0-9].[0-9]+.[0-9]+\*\*/Android AppsFlyer SDK \*\*v$android_sdk_version\*\*/g" README.md
-sed -i -e -r "s/iOS AppsFlyer SDK \*\*v[0-9].[0-9]+.[0-9]+\*\*/iOS AppsFlyer SDK \*\*v$ios_sdk_version\*\*/g" README.md
\ No newline at end of file
+ios_sdk_version=$(cat react-native-appsflyer.podspec | grep '\'AppsFlyerFramework\' | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+')
+android_sdk_version=$(cat android/build.gradle | grep 'com.appsflyer:af-android-sdk' | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+')
+sed -i -r "s/Android AppsFlyer SDK \*\*v[0-9]+\.[0-9]+\.[0-9]+\*\*/Android AppsFlyer SDK \*\*v$android_sdk_version\*\*/g" README.md
+sed -i -r "s/iOS AppsFlyer SDK \*\*v[0-9]+\.[0-9]+\.[0-9]+\*\*/iOS AppsFlyer SDK \*\*v$ios_sdk_version\*\*/g" README.md
diff --git a/.gitignore b/.gitignore
index ec07aad8..7bd61fdf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -38,6 +38,7 @@ android/gradlew.bat
android/gradle
android/gradlew
android/.project
+android/.settings
# node.js
#
@@ -66,4 +67,5 @@ demos/appsflyer-expo-app/.expo
demos/appsflyer-expo-app/node_modules
demos/appsflyer-expo-app/yarn.lock
+demos/appsflyer-react-native-app/ios/.xcode.env
demos/appsflyer-react-native-app/ios/AppsFlyerExample.xcodeproj/project.pbxproj
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7f4c90ff..a0741ce4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,9 @@
+## 6.15.1
+ Release date: *2024-09-15*
+
+- React Native >> Update Plugin to v6.15.1
+- React native plugin >> UDL failed deferred deep linking
+
## 6.14.3
Release date: *2024-04-28*
diff --git a/Docs/RN_ExpoDeepLinkIntegration.md b/Docs/RN_ExpoDeepLinkIntegration.md
index 939cc0c9..32a9b2a8 100644
--- a/Docs/RN_ExpoDeepLinkIntegration.md
+++ b/Docs/RN_ExpoDeepLinkIntegration.md
@@ -23,7 +23,7 @@ For more info please check out the [OneLinkβ’ Deep Linking Guide](https://suppo
1. In order to use AppsFlyer's deeplinks you need to configure intent filters/scheme/associatedDomains as described in [Expo's guide](https://docs.expo.dev/guides/linking/#universal-links-on-ios).
-2. **For Android apps:** You need to add `setIntent()` inside the `onNewIntent` method like described [here](RN_DeepLinkIntegrate.md#android-deeplink-setup). This plugin is NOT adding this code out the box, so you need to implement it **manually or with [custom config plugin](https://docs.expo.dev/modules/config-plugin-and-native-module-tutorial/#4-creating-a-new-config-plugin)**
+2. **For Android apps:** You need to add `setIntent()` inside the `onNewIntent` method like described [here](https://dev.appsflyer.com/hc/docs/rn_deeplinkintegrate#android-deeplink-setup). This plugin is NOT adding this code out the box, so you need to implement it **manually or with [custom config plugin](https://docs.expo.dev/modules/config-plugin-and-native-module-tutorial/#4-creating-a-new-config-plugin)**
## Full app.json example
diff --git a/Docs/RN_PurchaseConnector.md b/Docs/RN_PurchaseConnector.md
new file mode 100644
index 00000000..c2a88ccb
--- /dev/null
+++ b/Docs/RN_PurchaseConnector.md
@@ -0,0 +1,380 @@
+# React Native 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)
+
+π You can 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.*
+
+## 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 PurchaseConnector Files Without Opting In?](#important-callout-what-happens-if-you-use-the-purchase-connector-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 and In Apps Validation Result Listener](#android---subscription-and-inapps-validation-result-listener)
+ - [iOS Combined Validation Result Listener](#ios-combined-validation-result-listener)
+* [Testing the Integration](#testing-the-integration)
+ - [Android](#android)
+ - [iOS](#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 React Native plugin, such as the [`react-native-iap`](https://www.npmjs.com/package/react-native-iap) 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 React Native 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 React Native project, set `$AppsFlyerPurchaseConnector` to `true`.
+```ruby
+$AppsFlyerPurchaseConnector = true
+```
+For **Android**, in your `gradle.properties` file located within the `Android` folder of your React Native 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.
+
+### Important Callout: What Happens if You Use the Purchase Connector Files Without Opting In?
+
+The files for the Purchase Validation feature are always included in the plugin. If you try to use these JS 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 `PurchaseConnectorConfig` 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:
+
+```javascript
+import appsFlyer, {
+ AppsFlyerPurchaseConnector,
+ AppsFlyerPurchaseConnectorConfig,
+} from 'react-native-appsflyer';
+
+const purchaseConnectorConfig: PurchaseConnectorConfig = AppsFlyerPurchaseConnectorConfig.setConfig({
+ logSubscriptions: true,
+ logInApps: true,
+ sandbox: true,
+ });
+
+//Create the object
+AppsFlyerPurchaseConnector.create(purchaseConnectorConfig);
+
+// Continue with your application logic...
+```
+
+**IMPORTANT**: The `PurchaseConnectorConfig` 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 `PurchaseConnectorConfig`. 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:
+
+```javascript
+ // Correct usage: Providing configuration at first instantiation
+ const purchaseConnectorConfig1: PurchaseConnectorConfig = AppsFlyerPurchaseConnectorConfig.setConfig({
+ logSubscriptions: true,
+ logInApps: true,
+ sandbox: true,
+ });
+
+ // Additional instantiations will ignore the provided configuration
+ // and will return the previously created instance.
+ const purchaseConnectorConfig2: PurchaseConnectorConfig = AppsFlyerPurchaseConnectorConfig.setConfig({
+ logSubscriptions: true,
+ logInApps: true,
+ sandbox: true,
+ });
+
+ // purchaseConnector1 and purchaseConnector2 point to the same instance
+ assert(purchaseConnectorConfig1 == purchaseConnectorConfig2);
+```
+
+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 `appsFlyer.startSdk()` [start](https://github.com/AppsFlyerSDK/appsflyer-react-native-plugin/blob/master/Docs/RN_API.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.
+```javascript
+ import appsFlyer, {
+ AppsFlyerPurchaseConnector,
+ AppsFlyerPurchaseConnectorConfig,
+ } from 'react-native-appsflyer';
+
+ appsFlyer.startSdk();
+ const purchaseConnectorConfig: PurchaseConnectorConfig = AppsFlyerPurchaseConnectorConfig.setConfig({
+ logSubscriptions: true,
+ logInApps: true,
+ sandbox: true,
+ });
+
+ //Create the object
+ AppsFlyerPurchaseConnector.create(purchaseConnectorConfig);
+ //Start listening to transactions
+ AppsFlyerPurchaseConnector.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
+
+```javascript
+ //Stop listening to transactions after startSDK and after creating the AppsFlyerPurchaseConnector
+ AppsFlyerPurchaseConnector.startObservingTransactions();
+```
+
+### 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.
+
+```javascript
+const purchaseConnectorConfig = {
+ logSubscriptions: true, // Set to true to enable logging of subscriptions
+ // ... other configuration options
+};
+```
+
+### 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.
+
+```javascript
+const purchaseConnectorConfig = {
+ logInApps: true, // Set to true to enable logging of in-app purchases
+ // ... other configuration options
+};
+```
+
+And integrating both options into the example you provided would look like this:
+
+```javascript
+const purchaseConnectorConfig = AppsFlyerPurchaseConnectorConfig.setConfig({
+ logSubscriptions: true, // Enable automatic logging of subscription events
+ logInApps: true, // Enable automatic logging of in-app purchase events
+ sandbox: true, // Additional configuration option
+});
+```
+
+## 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 React Native plugin acts as a bridge between your React Native 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 `OnReceivePurchaseRevenueValidationInfo`.
+
+On the other hand, Android segregates callbacks for subscriptions and in-app purchases. It provides two separate listener methods - `onSubscriptionValidationResultSuccess` and `onSubscriptionValidationResultFailure` for subscriptions and `onInAppValidationResultSuccess` and `onInAppValidationResultFailure` 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 React Native 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 and In Apps Validation Result Listener
+
+```javascript
+import appsFlyer , {AppsFlyerPurchaseConnector} from 'react-native-appsflyer';
+
+ const handleValidationSuccess = (validationResult) => {
+ console.log('>> ValidationSuccess: ', validationResult);
+ };
+
+ const handleValidationFailure = (validationResult) => {
+ console.log('>> ValidationFailure: ', validationResult);
+ }
+
+ const handleSubscriptionValidationSuccess = (subscriptionValidationResult) => {
+ console.log('>> handleSubscriptionValidationSuccess: ', subscriptionValidationResult);
+ };
+
+ const handleSubscriptionValidationFailure = (subscriptionValidationResult) => {
+ console.log('>> handleSubscriptionValidationFailure: ', subscriptionValidationResult);
+ }
+
+ useEffect(() => {
+ let validationSuccessListener;
+ let validationFailureListener;
+ let subscriptionValidationSuccessListener;
+ let subscriptionValidationFailureListener;
+
+ if (Platform.OS === 'android') {
+ validationSuccessListener = AppsFlyerPurchaseConnector.onInAppValidationResultSuccess(handleValidationSuccess);
+ validationFailureListener = AppsFlyerPurchaseConnector.onInAppValidationResultFailure(handleValidationFailure);
+ subscriptionValidationSuccessListener = AppsFlyerPurchaseConnector.onSubscriptionValidationResultSuccess(handleSubscriptionValidationSuccess);
+ subscriptionValidationFailureListener = AppsFlyerPurchaseConnector.onSubscriptionValidationResultFailure(handleSubscriptionValidationFailure);
+ }
+
+ }, []);
+```
+
+### iOS Combined Validation Result Listener
+```javascript
+import appsFlyer , {AppsFlyerPurchaseConnector} from 'react-native-appsflyer';
+
+const handleOnReceivePurchaseRevenueValidationInfo = (validationInfo, error) => {
+ if (error) {
+ console.error("Error during purchase validation:", error);
+ } else {
+ console.log("Validation Info:", validationInfo);
+ }
+ }
+
+ useEffect(() => {
+ let purchaseRevenueValidationListener;
+
+ if (Platform.OS === 'ios') {
+ purchaseRevenueValidationListener = AppsFlyerPurchaseConnector.OnReceivePurchaseRevenueValidationInfo(handleOnReceivePurchaseRevenueValidationInfo);
+ }
+ };
+ }, []);
+```
+
+
+## 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 React Native, just set the `sandbox` parameter in the `PurchaseConnectorConfig` 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! *
+
+## 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
+```javascript
+const purchaseConnectorConfig: PurchaseConnectorConfig = AppsFlyerPurchaseConnectorConfig.setConfig({
+ logSubscriptions: true,
+ logInApps: true,
+ sandbox: true,
+ });
+
+ AppsFlyerPurchaseConnector.create(
+ purchaseConnectorConfig,
+ );
+
+const handleValidationSuccess = (validationResult) => {
+ console.log('>> ValidationSuccess: ', validationResult);
+ };
+
+ const handleValidationFailure = (validationResult) => {
+ console.log('>> ValidationFailure: ', validationResult);
+ }
+
+ const handleSubscriptionValidationSuccess = (subscriptionValidationResult) => {
+ console.log('>> handleSubscriptionValidationSuccess: ', subscriptionValidationResult);
+ };
+
+ const handleSubscriptionValidationFailure = (subscriptionValidationResult) => {
+ console.log('>> handleSubscriptionValidationFailure: ', subscriptionValidationResult);
+ }
+
+ const handleOnReceivePurchaseRevenueValidationInfo = (validationInfo, error) => {
+ if (error) {
+ console.error("Error during purchase validation:", error);
+ } else {
+ console.log("Validation Info:", validationInfo);
+ }
+ }
+
+
+ useEffect(() => {
+ let validationSuccessListener;
+ let validationFailureListener;
+ let subscriptionValidationSuccessListener;
+ let subscriptionValidationFailureListener;
+ let purchaseRevenueValidationListener;
+
+ if (Platform.OS === 'android') {
+ validationSuccessListener = AppsFlyerPurchaseConnector.onInAppValidationResultSuccess(handleValidationSuccess);
+ validationFailureListener = AppsFlyerPurchaseConnector.onInAppValidationResultFailure(handleValidationFailure);
+ subscriptionValidationSuccessListener = AppsFlyerPurchaseConnector.onSubscriptionValidationResultSuccess(handleSubscriptionValidationSuccess);
+ subscriptionValidationFailureListener = AppsFlyerPurchaseConnector.onSubscriptionValidationResultFailure(handleSubscriptionValidationFailure);
+ } else {
+ console.log('>> Creating purchaseRevenueValidationListener ');
+ purchaseRevenueValidationListener = AppsFlyerPurchaseConnector.OnReceivePurchaseRevenueValidationInfo(handleOnReceivePurchaseRevenueValidationInfo);
+ }
+
+ // Cleanup function
+ return () => {
+ if (Platform.OS === 'android') {
+ if (validationSuccessListener) validationSuccessListener.remove();
+ if (validationFailureListener) validationFailureListener.remove();
+ if (subscriptionValidationSuccessListener) subscriptionValidationSuccessListener.remove();
+ if (subscriptionValidationFailureListener) subscriptionValidationFailureListener.remove();
+ } else {
+ if (purchaseRevenueValidationListener) purchaseRevenueValidationListener.remove();
+ }
+ };
+ }, []);
+
+ AppsFlyerPurchaseConnector.startObservingTransactions();
+
+//AppsFlyerPurchaseConnector.stopObservingTransactions();
+```
\ No newline at end of file
diff --git a/Docs/RN_UnifiedDeepLink.md b/Docs/RN_UnifiedDeepLink.md
index 0d3baccf..a7561941 100644
--- a/Docs/RN_UnifiedDeepLink.md
+++ b/Docs/RN_UnifiedDeepLink.md
@@ -6,6 +6,10 @@ order: 9
hidden: false
---
+> π **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`.
+
### UDL flow
1. The SDK is triggered by:
diff --git a/Docs/RN_UserInvite.md b/Docs/RN_UserInvite.md
index dbb7750e..e0099286 100644
--- a/Docs/RN_UserInvite.md
+++ b/Docs/RN_UserInvite.md
@@ -19,7 +19,11 @@ The link that is generated for the user invite will use this OneLink ID as the b
| oneLinkID | string | oneLinkID |
| callback | function | success callback |
-
+ > π Note
+ >
+ > - Make sure to call `setAppInviteOneLinkID()` **before** calling `start`.
+ > - The OneLink template must be assigned to the app.
+
##### 2. `generateInviteLink(parameters, success, error)`
A complete list of supported parameters is available [here](https://support.appsflyer.com/hc/en-us/articles/115004480866-User-Invite-Tracking). Custom parameters can be passed using a userParams{} nested object, as in the example above.
diff --git a/PurchaseConnector/constants/constants.ts b/PurchaseConnector/constants/constants.ts
new file mode 100644
index 00000000..0fb5f6e4
--- /dev/null
+++ b/PurchaseConnector/constants/constants.ts
@@ -0,0 +1,15 @@
+class AppsFlyerConstants {
+ // Adding method constants
+ static readonly SUBSCRIPTION_VALIDATION_SUCCESS: string = 'subscriptionValidationSuccess';
+ static readonly SUBSCRIPTION_VALIDATION_FAILURE: string = 'subscriptionValidationFailure';
+ static readonly IN_APP_PURCHASE_VALIDATION_SUCCESS: string = 'inAppPurchaseValidationSuccess';
+ static readonly IN_APP_PURCHASE_VALIDATION_FAILURE: string = 'inAppPurchaseValidationFailure';
+ static readonly DID_RECEIVE_PURCHASE_REVENUE_VALIDATION_INFO: string =
+ "onDidReceivePurchaseRevenueValidationInfo";
+
+ // Adding key constants
+ static readonly RESULT: string = "result";
+ static readonly ERROR: string = "error";
+ }
+
+ export default AppsFlyerConstants;
\ No newline at end of file
diff --git a/PurchaseConnector/models/auto_renewing_plan.ts b/PurchaseConnector/models/auto_renewing_plan.ts
new file mode 100644
index 00000000..fe84bdfd
--- /dev/null
+++ b/PurchaseConnector/models/auto_renewing_plan.ts
@@ -0,0 +1,34 @@
+import { SubscriptionItemPriceChangeDetailsJson, SubscriptionItemPriceChangeDetails } from "./subscription_item_price_change_details";
+
+export type AutoRenewingPlanJson = {
+ autoRenewEnabled?: boolean;
+ priceChangeDetails?: SubscriptionItemPriceChangeDetailsJson;
+};
+
+export class AutoRenewingPlan {
+ autoRenewEnabled?: boolean;
+ priceChangeDetails?: SubscriptionItemPriceChangeDetails;
+
+ constructor(
+ autoRenewEnabled?: boolean,
+ priceChangeDetails?: SubscriptionItemPriceChangeDetails
+ ) {
+ this.autoRenewEnabled = autoRenewEnabled;
+ this.priceChangeDetails = priceChangeDetails;
+ }
+
+ static fromJson(json: AutoRenewingPlanJson): AutoRenewingPlan {
+ return new AutoRenewingPlan(
+ json.autoRenewEnabled,
+ json.priceChangeDetails &&
+ SubscriptionItemPriceChangeDetails.fromJson(json.priceChangeDetails)
+ );
+ }
+
+ toJson(): AutoRenewingPlanJson {
+ return {
+ autoRenewEnabled: this.autoRenewEnabled,
+ priceChangeDetails: this.priceChangeDetails?.toJson(),
+ };
+ }
+}
diff --git a/PurchaseConnector/models/canceled_state_context.ts b/PurchaseConnector/models/canceled_state_context.ts
new file mode 100644
index 00000000..17f5f075
--- /dev/null
+++ b/PurchaseConnector/models/canceled_state_context.ts
@@ -0,0 +1,141 @@
+export class CanceledStateContext {
+ developerInitiatedCancellation?: DeveloperInitiatedCancellation;
+ replacementCancellation?: ReplacementCancellation;
+ systemInitiatedCancellation?: SystemInitiatedCancellation;
+ userInitiatedCancellation?: UserInitiatedCancellation;
+
+ constructor(
+ developerInitiatedCancellation?: DeveloperInitiatedCancellation,
+ replacementCancellation?: ReplacementCancellation,
+ systemInitiatedCancellation?: SystemInitiatedCancellation,
+ userInitiatedCancellation?: UserInitiatedCancellation
+ ) {
+ this.developerInitiatedCancellation = developerInitiatedCancellation;
+ this.replacementCancellation = replacementCancellation;
+ this.systemInitiatedCancellation = systemInitiatedCancellation;
+ this.userInitiatedCancellation = userInitiatedCancellation;
+ }
+
+ static fromJson(json: any): CanceledStateContext {
+ return new CanceledStateContext(
+ json.developerInitiatedCancellation != null
+ ? DeveloperInitiatedCancellation.fromJson(
+ json.developerInitiatedCancellation
+ )
+ : undefined,
+ json.replacementCancellation != null
+ ? ReplacementCancellation.fromJson(json.replacementCancellation)
+ : undefined,
+ json.systemInitiatedCancellation != null
+ ? SystemInitiatedCancellation.fromJson(json.systemInitiatedCancellation)
+ : undefined,
+ json.userInitiatedCancellation != null
+ ? UserInitiatedCancellation.fromJson(json.userInitiatedCancellation)
+ : undefined
+ );
+ }
+
+ toJson(): Record {
+ return {
+ developerInitiatedCancellation:
+ this.developerInitiatedCancellation?.toJson(),
+ replacementCancellation: this.replacementCancellation?.toJson(),
+ systemInitiatedCancellation: this.systemInitiatedCancellation?.toJson(),
+ userInitiatedCancellation: this.userInitiatedCancellation?.toJson(),
+ };
+ }
+}
+
+/**
+ * TODO: Need to check each state context further...
+ */
+class DeveloperInitiatedCancellation {
+ constructor() {}
+
+ static fromJson(json: any): DeveloperInitiatedCancellation {
+ // Here you would implement the conversion from JSON to DeveloperInitiatedCancellation instance
+ return new DeveloperInitiatedCancellation();
+ }
+
+ toJson(): Record {
+ // Here you would implement the conversion from DeveloperInitiatedCancellation instance to JSON
+ return {};
+ }
+}
+
+class ReplacementCancellation {
+ constructor() {}
+
+ static fromJson(json: any): ReplacementCancellation {
+ // Here you would implement the conversion from JSON to ReplacementCancellation instance
+ return new ReplacementCancellation();
+ }
+
+ toJson(): Record {
+ return {};
+ }
+}
+
+class SystemInitiatedCancellation {
+ constructor() {}
+
+ static fromJson(json: any): SystemInitiatedCancellation {
+ // Here you would implement the conversion from JSON to SystemInitiatedCancellation instance
+ return new SystemInitiatedCancellation();
+ }
+
+ toJson(): Record {
+ // Here you would implement the conversion from SystemInitiatedCancellation instance to JSON
+ return {};
+ }
+}
+
+class UserInitiatedCancellation {
+ cancelSurveyResult?: CancelSurveyResult; // Made optional as per Dart's CancelSurveyResult? declaration
+ cancelTime: string;
+
+ constructor(
+ cancelSurveyResult: CancelSurveyResult | undefined,
+ cancelTime: string
+ ) {
+ this.cancelSurveyResult = cancelSurveyResult;
+ this.cancelTime = cancelTime;
+ }
+
+ static fromJson(json: any): UserInitiatedCancellation {
+ return new UserInitiatedCancellation(
+ json.cancelSurveyResult != null
+ ? CancelSurveyResult.fromJson(json.cancelSurveyResult)
+ : undefined,
+ json.cancelTime
+ );
+ }
+
+ toJson(): Record {
+ return {
+ cancelSurveyResult: this.cancelSurveyResult?.toJson(),
+ cancelTime: this.cancelTime,
+ };
+ }
+}
+
+class CancelSurveyResult {
+ reason: string;
+ reasonUserInput: string;
+
+ constructor(reason: string, reasonUserInput: string) {
+ this.reason = reason;
+ this.reasonUserInput = reasonUserInput;
+ }
+
+ static fromJson(json: any): CancelSurveyResult {
+ return new CancelSurveyResult(json.reason, json.reasonUserInput);
+ }
+
+ toJson(): Record {
+ return {
+ reason: this.reason,
+ reasonUserInput: this.reasonUserInput,
+ };
+ }
+}
diff --git a/PurchaseConnector/models/deferred_item_replacement.ts b/PurchaseConnector/models/deferred_item_replacement.ts
new file mode 100644
index 00000000..5550a18b
--- /dev/null
+++ b/PurchaseConnector/models/deferred_item_replacement.ts
@@ -0,0 +1,21 @@
+export type DeferredItemReplacementJson = {
+ productId: string;
+};
+
+export class DeferredItemReplacement {
+ productId: string;
+
+ constructor(productId: string) {
+ this.productId = productId;
+ }
+
+ static fromJson(json: DeferredItemReplacementJson): DeferredItemReplacement {
+ return new DeferredItemReplacement(json.productId);
+ }
+
+ toJson(): DeferredItemReplacementJson {
+ return {
+ productId: this.productId,
+ };
+ }
+}
diff --git a/PurchaseConnector/models/external_account_identifiers.ts b/PurchaseConnector/models/external_account_identifiers.ts
new file mode 100644
index 00000000..9ee3f0bf
--- /dev/null
+++ b/PurchaseConnector/models/external_account_identifiers.ts
@@ -0,0 +1,39 @@
+export type ExternalAccountIdentifiersJson = {
+ externalAccountId: string;
+ obfuscatedExternalAccountId: string;
+ obfuscatedExternalProfileId: string;
+};
+
+export class ExternalAccountIdentifiers {
+ externalAccountId: string;
+ obfuscatedExternalAccountId: string;
+ obfuscatedExternalProfileId: string;
+
+ constructor(
+ externalAccountId: string,
+ obfuscatedExternalAccountId: string,
+ obfuscatedExternalProfileId: string
+ ) {
+ this.externalAccountId = externalAccountId;
+ this.obfuscatedExternalAccountId = obfuscatedExternalAccountId;
+ this.obfuscatedExternalProfileId = obfuscatedExternalProfileId;
+ }
+
+ static fromJson(
+ json: ExternalAccountIdentifiersJson
+ ): ExternalAccountIdentifiers {
+ return new ExternalAccountIdentifiers(
+ json.externalAccountId,
+ json.obfuscatedExternalAccountId,
+ json.obfuscatedExternalProfileId
+ );
+ }
+
+ toJson(): ExternalAccountIdentifiersJson {
+ return {
+ externalAccountId: this.externalAccountId,
+ obfuscatedExternalAccountId: this.obfuscatedExternalAccountId,
+ obfuscatedExternalProfileId: this.obfuscatedExternalProfileId,
+ };
+ }
+}
diff --git a/PurchaseConnector/models/in_app_purchase_validation_result.ts b/PurchaseConnector/models/in_app_purchase_validation_result.ts
new file mode 100644
index 00000000..5bad94b7
--- /dev/null
+++ b/PurchaseConnector/models/in_app_purchase_validation_result.ts
@@ -0,0 +1,34 @@
+import { ProductPurchase } from "./product_purchase";
+import { ValidationFailureData } from "./validation_failure_data";
+
+export default class InAppPurchaseValidationResult {
+ success: boolean;
+ productPurchase?: ProductPurchase;
+ failureData?: ValidationFailureData;
+
+ constructor(
+ success: boolean,
+ productPurchase?: ProductPurchase,
+ failureData?: ValidationFailureData
+ ) {
+ this.success = success;
+ this.productPurchase = productPurchase;
+ this.failureData = failureData;
+ }
+
+ static fromJson(json: any): InAppPurchaseValidationResult {
+ return new InAppPurchaseValidationResult(
+ json.success,
+ json.productPurchase,
+ json.failureData
+ );
+ }
+
+ toJson(): any {
+ return {
+ success: this.success,
+ productPurchase: this.productPurchase,
+ failureData: this.failureData,
+ };
+ }
+ }
\ No newline at end of file
diff --git a/PurchaseConnector/models/ios_errors.ts b/PurchaseConnector/models/ios_errors.ts
new file mode 100644
index 00000000..aa6231d1
--- /dev/null
+++ b/PurchaseConnector/models/ios_errors.ts
@@ -0,0 +1,52 @@
+// TypeScript class for IOS Error
+export class IosError {
+ localizedDescription: string;
+ domain: string;
+ code: number;
+
+ constructor(localizedDescription: string, domain: string, code: number) {
+ this.localizedDescription = localizedDescription;
+ this.domain = domain;
+ this.code = code;
+ }
+
+ // Converts the class instance to a JSON object
+ toJson(): object {
+ return {
+ localizedDescription: this.localizedDescription,
+ domain: this.domain,
+ code: this.code,
+ };
+ }
+
+ // Creates an instance of the class from a JSON object
+ static fromJson(json: any): IosError {
+ return new IosError(json.localizedDescription, json.domain, json.code);
+ }
+}
+
+/**
+ * Usage example:
+ * // Creating an instance of IosError
+ * const iosError = new IosError('An error occurred.', 'com.example.domain', 100);
+ *
+ * // Display information about the IOS error
+ * console.log(iosError.localizedDescription); // Outputs: An error occurred.
+ * console.log(iosError.domain); // Outputs: com.example.domain
+ * console.log(iosError.code); // Outputs: 100
+ *
+ * // Serializing IosError instance to a JSON object
+ * const iosErrorJson = iosError.toJson();
+ * console.log(iosErrorJson); // Outputs: { localizedDescription: 'An error occurred.', domain: 'com.example.domain', code: 100 }
+ *
+ * // Sample JSON objects
+ * const iosErrorData = {
+ * localizedDescription: 'A network error occurred.',
+ * domain: 'com.example.network',
+ * code: 404
+ * };
+ *
+ * // Deserializing the parsed JSON into instance of IosError
+ * const deserializedIosError = IosError.fromJson(iosErrorData);
+ * console.log(deserializedIosError);
+ */
diff --git a/PurchaseConnector/models/jvm_throwable.ts b/PurchaseConnector/models/jvm_throwable.ts
new file mode 100644
index 00000000..3ac640cc
--- /dev/null
+++ b/PurchaseConnector/models/jvm_throwable.ts
@@ -0,0 +1,66 @@
+// TypeScript class for JVM Throwable
+export class JVMThrowable {
+ type: string;
+ message: string;
+ stacktrace: string;
+ cause: JVMThrowable | null;
+
+ constructor(
+ type: string,
+ message: string,
+ stacktrace: string,
+ cause: JVMThrowable | null
+ ) {
+ this.type = type;
+ this.message = message;
+ this.stacktrace = stacktrace;
+ this.cause = cause;
+ }
+
+ // Converts the class instance to a JSON object
+ toJson(): object {
+ return {
+ type: this.type,
+ message: this.message,
+ stacktrace: this.stacktrace,
+ cause: this.cause?.toJson(),
+ };
+ }
+
+ // Creates an instance of the class from a JSON object
+ static fromJson(json: any): JVMThrowable {
+ return new JVMThrowable(
+ json.type,
+ json.message,
+ json.stacktrace,
+ json.cause ? JVMThrowable.fromJson(json.cause) : null
+ );
+ }
+}
+
+/**
+ * Usage example:
+ * // Creating an instance of JVMThrowable
+ * const jvmThrowable = new JVMThrowable('ExceptionType', 'An exception occurred', 'stacktraceString', null);
+ *
+ * // Display information about the JVM throwable
+ * console.log(jvmThrowable.type); // Outputs: ExceptionType
+ * console.log(jvmThrowable.message); // Outputs: An exception occurred
+ * console.log(jvmThrowable.stacktrace); // Outputs: stacktraceString
+ * console.log(jvmThrowable.cause); // Outputs: null (since no cause is provided here)
+ *
+ * // Serializing JVMThrowable instance to a JSON object
+ * const jvmThrowableJson = jvmThrowable.toJson();
+ * console.log(jvmThrowableJson); // Outputs: { type: 'ExceptionType', message: 'An exception occurred', stacktrace: 'stacktraceString', cause: null }
+ *
+ * const jvmThrowableData = {
+ * type: 'RuntimeException',
+ * message: 'Failed to load resource',
+ * stacktrace: 'stacktrace info here',
+ * cause: null // Alternatively, you could nest another throwable here if there is one
+ * };
+ *
+ * // Deserializing the parsed JSON into instance JVMThrowable
+ * const deserializedJVMThrowable = JVMThrowable.fromJson(jvmThrowableData);
+ * console.log(deserializedJVMThrowable);
+ */
diff --git a/PurchaseConnector/models/missing_configuration_exception.ts b/PurchaseConnector/models/missing_configuration_exception.ts
new file mode 100644
index 00000000..2cd4c8cd
--- /dev/null
+++ b/PurchaseConnector/models/missing_configuration_exception.ts
@@ -0,0 +1,5 @@
+export class MissingConfigurationException extends Error {
+ constructor() {
+ super("Missing configuration for PurchaseConnector");
+ }
+}
diff --git a/PurchaseConnector/models/money_model.ts b/PurchaseConnector/models/money_model.ts
new file mode 100644
index 00000000..d61ab985
--- /dev/null
+++ b/PurchaseConnector/models/money_model.ts
@@ -0,0 +1,29 @@
+export type MoneyArgs = {
+ currencyCode: string;
+ nanos: number;
+ units: number;
+};
+
+export class Money {
+ currencyCode: string;
+ nanos: number;
+ units: number;
+
+ constructor(currencyCode: string, nanos: number, units: number) {
+ this.currencyCode = currencyCode;
+ this.nanos = nanos;
+ this.units = units;
+ }
+
+ static fromJson(json: MoneyArgs): Money {
+ return new Money(json.currencyCode, json.nanos, json.units);
+ }
+
+ toJson(): MoneyArgs {
+ return {
+ currencyCode: this.currencyCode,
+ nanos: this.nanos,
+ units: this.units,
+ };
+ }
+}
diff --git a/PurchaseConnector/models/offer_details.ts b/PurchaseConnector/models/offer_details.ts
new file mode 100644
index 00000000..e9de947a
--- /dev/null
+++ b/PurchaseConnector/models/offer_details.ts
@@ -0,0 +1,33 @@
+export type OfferDetailsJson = {
+ offerTags?: string[];
+ basePlanId: string;
+ offerId?: string;
+};
+
+export class OfferDetails {
+ offerTags?: string[];
+ basePlanId: string;
+ offerId?: string;
+
+ constructor(
+ offerTags: string[] | undefined,
+ basePlanId: string,
+ offerId: string | undefined
+ ) {
+ this.offerTags = offerTags;
+ this.basePlanId = basePlanId;
+ this.offerId = offerId;
+ }
+
+ static fromJson(json: OfferDetailsJson): OfferDetails {
+ return new OfferDetails(json.offerTags, json.basePlanId, json.offerId);
+ }
+
+ toJson(): OfferDetailsJson {
+ return {
+ offerTags: this.offerTags,
+ basePlanId: this.basePlanId,
+ offerId: this.offerId,
+ };
+ }
+}
diff --git a/PurchaseConnector/models/paused_state_context.ts b/PurchaseConnector/models/paused_state_context.ts
new file mode 100644
index 00000000..522edfc8
--- /dev/null
+++ b/PurchaseConnector/models/paused_state_context.ts
@@ -0,0 +1,21 @@
+type PausedStateContextJson = {
+ autoResumeTime: string;
+};
+
+export class PausedStateContext {
+ autoResumeTime: string;
+
+ constructor(autoResumeTime: string) {
+ this.autoResumeTime = autoResumeTime;
+ }
+
+ static fromJson(json: PausedStateContextJson): PausedStateContext {
+ return new PausedStateContext(json.autoResumeTime);
+ }
+
+ toJson(): PausedStateContextJson {
+ return {
+ autoResumeTime: this.autoResumeTime,
+ };
+ }
+}
diff --git a/PurchaseConnector/models/prepaid_plan.ts b/PurchaseConnector/models/prepaid_plan.ts
new file mode 100644
index 00000000..40d967e4
--- /dev/null
+++ b/PurchaseConnector/models/prepaid_plan.ts
@@ -0,0 +1,21 @@
+export type PrepaidPlanJson = {
+ allowExtendAfterTime?: string;
+};
+
+export class PrepaidPlan {
+ allowExtendAfterTime?: string;
+
+ constructor(allowExtendAfterTime?: string) {
+ this.allowExtendAfterTime = allowExtendAfterTime;
+ }
+
+ static fromJson(json: PrepaidPlanJson): PrepaidPlan {
+ return new PrepaidPlan(json.allowExtendAfterTime);
+ }
+
+ toJson(): PrepaidPlanJson {
+ return {
+ allowExtendAfterTime: this.allowExtendAfterTime,
+ };
+ }
+}
diff --git a/PurchaseConnector/models/product_purchase.ts b/PurchaseConnector/models/product_purchase.ts
new file mode 100644
index 00000000..fd21ba0c
--- /dev/null
+++ b/PurchaseConnector/models/product_purchase.ts
@@ -0,0 +1,99 @@
+export type ProductPurchaseArgs = {
+ kind: string;
+ purchaseTimeMillis: string;
+ purchaseState: number;
+ consumptionState: number;
+ developerPayload: string;
+ orderId: string;
+ purchaseType: number;
+ acknowledgementState: number;
+ purchaseToken: string;
+ productId: string;
+ quantity: number;
+ obfuscatedExternalAccountId: string;
+ obfuscatedExternalProfileId: string;
+ regionCode: string;
+};
+
+export class ProductPurchase {
+ kind: string;
+ purchaseTimeMillis: string;
+ purchaseState: number;
+ consumptionState: number;
+ developerPayload: string;
+ orderId: string;
+ purchaseType: number;
+ acknowledgementState: number;
+ purchaseToken: string;
+ productId: string;
+ quantity: number;
+ obfuscatedExternalAccountId: string;
+ obfuscatedExternalProfileId: string;
+ regionCode: string;
+
+ constructor(args: ProductPurchaseArgs) {
+ this.kind = args.kind;
+ this.purchaseTimeMillis = args.purchaseTimeMillis;
+ this.purchaseState = args.purchaseState;
+ this.consumptionState = args.consumptionState;
+ this.developerPayload = args.developerPayload;
+ this.orderId = args.orderId;
+ this.purchaseType = args.purchaseType;
+ this.acknowledgementState = args.acknowledgementState;
+ this.purchaseToken = args.purchaseToken;
+ this.productId = args.productId;
+ this.quantity = args.quantity;
+ this.obfuscatedExternalAccountId = args.obfuscatedExternalAccountId;
+ this.obfuscatedExternalProfileId = args.obfuscatedExternalProfileId;
+ this.regionCode = args.regionCode;
+ }
+
+ toJson(): Record {
+ return {
+ kind: this.kind,
+ purchaseTimeMillis: this.purchaseTimeMillis,
+ purchaseState: this.purchaseState,
+ consumptionState: this.consumptionState,
+ developerPayload: this.developerPayload,
+ orderId: this.orderId,
+ purchaseType: this.purchaseType,
+ acknowledgementState: this.acknowledgementState,
+ purchaseToken: this.purchaseToken,
+ productId: this.productId,
+ quantity: this.quantity,
+ obfuscatedExternalAccountId: this.obfuscatedExternalAccountId,
+ obfuscatedExternalProfileId: this.obfuscatedExternalProfileId,
+ regionCode: this.regionCode,
+ };
+ }
+
+ static fromJson(json: any): ProductPurchase {
+ return new ProductPurchase({
+ kind: json.kind as string,
+ purchaseTimeMillis: json.purchaseTimeMillis as string,
+ purchaseState: json.purchaseState as number,
+ consumptionState: json.consumptionState as number,
+ developerPayload: json.developerPayload as string,
+ orderId: json.orderId as string,
+ purchaseType: json.purchaseType as number,
+ acknowledgementState: json.acknowledgementState as number,
+ purchaseToken: json.purchaseToken as string,
+ productId: json.productId as string,
+ quantity: json.quantity as number,
+ obfuscatedExternalAccountId: json.obfuscatedExternalAccountId as string,
+ obfuscatedExternalProfileId: json.obfuscatedExternalProfileId as string,
+ regionCode: json.regionCode as string,
+ });
+ }
+}
+
+/**
+ * // Usage:
+ * // To convert to JSON:
+ * const productPurchaseInstance = new ProductPurchase({ ...args });
+ * const json = productPurchaseInstance.toJson();
+ *
+ * // To convert from JSON:
+ * const json = {...json };
+ * const productPurchaseInstance = ProductPurchase.fromJson(json);
+ */
diff --git a/PurchaseConnector/models/subscribe_with_google_info.ts b/PurchaseConnector/models/subscribe_with_google_info.ts
new file mode 100644
index 00000000..43737d6d
--- /dev/null
+++ b/PurchaseConnector/models/subscribe_with_google_info.ts
@@ -0,0 +1,44 @@
+export type SubscribeWithGoogleInfoJson = {
+ emailAddress: string;
+ familyName: string;
+ givenName: string;
+ profileId: string;
+ profileName: string;
+};
+
+export class SubscribeWithGoogleInfo {
+ emailAddress: string;
+ familyName: string;
+ givenName: string;
+ profileId: string;
+ profileName: string;
+
+ constructor(
+ emailAddress: string,
+ familyName: string,
+ givenName: string,
+ profileId: string,
+ profileName: string
+ ) {
+ this.emailAddress = emailAddress;
+ this.familyName = familyName;
+ this.givenName = givenName;
+ this.profileId = profileId;
+ this.profileName = profileName;
+ }
+
+ static fromJson(json: SubscribeWithGoogleInfoJson): SubscribeWithGoogleInfo {
+ return new SubscribeWithGoogleInfo(
+ json.emailAddress,
+ json.familyName,
+ json.givenName,
+ json.profileId,
+ json.profileName
+ );
+ }
+
+ //This is primitve types so we can use ...this (spread operator)
+ toJson(): SubscribeWithGoogleInfoJson {
+ return { ...this };
+ }
+}
diff --git a/PurchaseConnector/models/subscription_item_price_change_details.ts b/PurchaseConnector/models/subscription_item_price_change_details.ts
new file mode 100644
index 00000000..53ce6430
--- /dev/null
+++ b/PurchaseConnector/models/subscription_item_price_change_details.ts
@@ -0,0 +1,51 @@
+import { Money, MoneyArgs } from "./money_model";
+
+export type SubscriptionItemPriceChangeDetailsJson = {
+ expectedNewPriceChargeTime: string;
+ priceChangeMode: string;
+ priceChangeState: string;
+ newPrice?: MoneyArgs;
+};
+
+export class SubscriptionItemPriceChangeDetails {
+ expectedNewPriceChargeTime: string;
+ priceChangeMode: string;
+ priceChangeState: string;
+ newPrice?: Money;
+
+ constructor(
+ expectedNewPriceChargeTime: string,
+ priceChangeMode: string,
+ priceChangeState: string,
+ newPrice?: Money
+ ) {
+ this.expectedNewPriceChargeTime = expectedNewPriceChargeTime;
+ this.priceChangeMode = priceChangeMode;
+ this.priceChangeState = priceChangeState;
+ this.newPrice = newPrice;
+ }
+
+ static fromJson(
+ json: SubscriptionItemPriceChangeDetailsJson
+ ): SubscriptionItemPriceChangeDetails {
+ const newPriceInstance = json.newPrice
+ ? Money.fromJson(json.newPrice)
+ : undefined;
+
+ return new SubscriptionItemPriceChangeDetails(
+ json.expectedNewPriceChargeTime,
+ json.priceChangeMode,
+ json.priceChangeState,
+ newPriceInstance
+ );
+ }
+
+ toJson(): SubscriptionItemPriceChangeDetailsJson {
+ return {
+ expectedNewPriceChargeTime: this.expectedNewPriceChargeTime,
+ priceChangeMode: this.priceChangeMode,
+ priceChangeState: this.priceChangeState,
+ newPrice: this.newPrice ? this.newPrice.toJson() : undefined,
+ };
+ }
+}
diff --git a/PurchaseConnector/models/subscription_purchase.ts b/PurchaseConnector/models/subscription_purchase.ts
new file mode 100644
index 00000000..4c67ab15
--- /dev/null
+++ b/PurchaseConnector/models/subscription_purchase.ts
@@ -0,0 +1,91 @@
+import { CanceledStateContext } from "./canceled_state_context";
+import { ExternalAccountIdentifiers } from "./external_account_identifiers";
+import { PausedStateContext } from "./paused_state_context";
+import { SubscribeWithGoogleInfo } from "./subscribe_with_google_info";
+import { SubscriptionPurchaseLineItem } from "./subscription_purchase_line_item";
+import { TestPurchase } from "./test_purchase";
+
+type SubscriptionPurchaseArgs = {
+ acknowledgementState: string;
+ canceledStateContext?: CanceledStateContext;
+ externalAccountIdentifiers?: ExternalAccountIdentifiers;
+ kind: string;
+ latestOrderId: string;
+ lineItems: SubscriptionPurchaseLineItem[];
+ linkedPurchaseToken?: string;
+ pausedStateContext?: PausedStateContext;
+ regionCode: string;
+ startTime: string;
+ subscribeWithGoogleInfo?: SubscribeWithGoogleInfo;
+ subscriptionState: string;
+ testPurchase?: TestPurchase;
+};
+
+export class SubscriptionPurchase {
+ acknowledgementState: string;
+ canceledStateContext?: CanceledStateContext;
+ externalAccountIdentifiers?: ExternalAccountIdentifiers;
+ kind: string;
+ latestOrderId: string;
+ lineItems: SubscriptionPurchaseLineItem[];
+ linkedPurchaseToken?: string;
+ pausedStateContext?: PausedStateContext;
+ regionCode: string;
+ startTime: string;
+ subscribeWithGoogleInfo?: SubscribeWithGoogleInfo;
+ subscriptionState: string;
+ testPurchase?: TestPurchase;
+
+ constructor(args: SubscriptionPurchaseArgs) {
+ this.acknowledgementState = args.acknowledgementState;
+ this.canceledStateContext = args.canceledStateContext;
+ this.externalAccountIdentifiers = args.externalAccountIdentifiers;
+ this.kind = args.kind;
+ this.latestOrderId = args.latestOrderId;
+ this.lineItems = args.lineItems;
+ this.linkedPurchaseToken = args.linkedPurchaseToken;
+ this.pausedStateContext = args.pausedStateContext;
+ this.regionCode = args.regionCode;
+ this.startTime = args.startTime;
+ this.subscribeWithGoogleInfo = args.subscribeWithGoogleInfo;
+ this.subscriptionState = args.subscriptionState;
+ this.testPurchase = args.testPurchase;
+ }
+
+ static fromJson(json: any): SubscriptionPurchase {
+ return new SubscriptionPurchase({
+ acknowledgementState: json.acknowledgementState as string,
+ canceledStateContext: json.canceledStateContext,
+ externalAccountIdentifiers: json.externalAccountIdentifiers,
+ kind: json.kind as string,
+ latestOrderId: json.latestOrderId as string,
+ lineItems: json.lineItems,
+ linkedPurchaseToken: json.linkedPurchaseToken as string,
+ pausedStateContext: json.pausedStateContext,
+ regionCode: json.regionCode as string,
+ startTime: json.startTime as string,
+ subscribeWithGoogleInfo: json.subscribeWithGoogleInfo,
+ subscriptionState: json.subscriptionState as string,
+ testPurchase: json.testPurchase,
+ });
+ }
+
+ toJson(): Record {
+ return {
+ acknowledgementState: this.acknowledgementState,
+ canceledStateContext: this.canceledStateContext,
+ externalAccountIdentifiers: this.externalAccountIdentifiers,
+ kind: this.kind,
+ latestOrderId: this.latestOrderId,
+ lineItems: this.lineItems,
+ linkedPurchaseToken: this.linkedPurchaseToken,
+ pausedStateContext: this.pausedStateContext,
+ regionCode: this.regionCode,
+ startTime: this.startTime,
+ subscribeWithGoogleInfo: this.subscribeWithGoogleInfo,
+ subscriptionState: this.subscriptionState,
+ testPurchase: this.testPurchase,
+ };
+ }
+}
+
diff --git a/PurchaseConnector/models/subscription_purchase_line_item.ts b/PurchaseConnector/models/subscription_purchase_line_item.ts
new file mode 100644
index 00000000..6d06d98c
--- /dev/null
+++ b/PurchaseConnector/models/subscription_purchase_line_item.ts
@@ -0,0 +1,66 @@
+import { AutoRenewingPlanJson, AutoRenewingPlan } from "./auto_renewing_plan";
+import { DeferredItemReplacementJson, DeferredItemReplacement } from "./deferred_item_replacement";
+import { OfferDetailsJson, OfferDetails } from "./offer_details";
+import { PrepaidPlanJson, PrepaidPlan } from "./prepaid_plan";
+
+export type SubscriptionPurchaseLineItemJson = {
+ productId: string;
+ expiryTime: string;
+ autoRenewingPlan?: AutoRenewingPlanJson;
+ deferredItemReplacement?: DeferredItemReplacementJson;
+ offerDetails?: OfferDetailsJson;
+ prepaidPlan?: PrepaidPlanJson;
+};
+
+export class SubscriptionPurchaseLineItem {
+ productId: string;
+ expiryTime: string;
+ autoRenewingPlan?: AutoRenewingPlan;
+ deferredItemReplacement?: DeferredItemReplacement;
+ offerDetails?: OfferDetails;
+ prepaidPlan?: PrepaidPlan;
+
+ constructor(
+ productId: string,
+ expiryTime: string,
+ autoRenewingPlan?: AutoRenewingPlan,
+ deferredItemReplacement?: DeferredItemReplacement,
+ offerDetails?: OfferDetails,
+ prepaidPlan?: PrepaidPlan
+ ) {
+ this.productId = productId;
+ this.expiryTime = expiryTime;
+ this.autoRenewingPlan = autoRenewingPlan;
+ this.deferredItemReplacement = deferredItemReplacement;
+ this.offerDetails = offerDetails;
+ this.prepaidPlan = prepaidPlan;
+ }
+
+ static fromJson(
+ json: SubscriptionPurchaseLineItemJson
+ ): SubscriptionPurchaseLineItem {
+ return new SubscriptionPurchaseLineItem(
+ json.productId,
+ json.expiryTime,
+ json.autoRenewingPlan
+ ? AutoRenewingPlan.fromJson(json.autoRenewingPlan)
+ : undefined,
+ json.deferredItemReplacement
+ ? DeferredItemReplacement.fromJson(json.deferredItemReplacement)
+ : undefined,
+ json.offerDetails ? OfferDetails.fromJson(json.offerDetails) : undefined,
+ json.prepaidPlan ? PrepaidPlan.fromJson(json.prepaidPlan) : undefined
+ );
+ }
+
+ toJson(): SubscriptionPurchaseLineItemJson {
+ return {
+ productId: this.productId,
+ expiryTime: this.expiryTime,
+ autoRenewingPlan: this.autoRenewingPlan?.toJson(),
+ deferredItemReplacement: this.deferredItemReplacement?.toJson(),
+ offerDetails: this.offerDetails?.toJson(),
+ prepaidPlan: this.prepaidPlan?.toJson(),
+ };
+ }
+}
diff --git a/PurchaseConnector/models/subscription_validation_result.ts b/PurchaseConnector/models/subscription_validation_result.ts
new file mode 100644
index 00000000..c989708c
--- /dev/null
+++ b/PurchaseConnector/models/subscription_validation_result.ts
@@ -0,0 +1,34 @@
+import { SubscriptionPurchase } from "./subscription_purchase";
+import { ValidationFailureData } from "./validation_failure_data";
+
+export default class SubscriptionValidationResult {
+ success: boolean;
+ subscriptionPurchase?: SubscriptionPurchase;
+ failureData?: ValidationFailureData;
+
+ constructor(
+ success: boolean,
+ subscriptionPurchase?: SubscriptionPurchase,
+ failureData?: ValidationFailureData
+ ) {
+ this.success = success;
+ this.subscriptionPurchase = subscriptionPurchase;
+ this.failureData = failureData;
+ }
+
+ static fromJson(json: any): SubscriptionValidationResult {
+ return new SubscriptionValidationResult(
+ json.success,
+ json.subscriptionPurchase,
+ json.failureData
+ );
+ }
+
+ toJson(): any {
+ return {
+ success: this.success,
+ subscriptionPurchase: this.subscriptionPurchase,
+ failureData: this.failureData,
+ };
+ }
+}
\ No newline at end of file
diff --git a/PurchaseConnector/models/test_purchase.ts b/PurchaseConnector/models/test_purchase.ts
new file mode 100644
index 00000000..0c37093d
--- /dev/null
+++ b/PurchaseConnector/models/test_purchase.ts
@@ -0,0 +1,13 @@
+interface TestPurchaseJson {}
+
+export class TestPurchase {
+ constructor() {}
+
+ static fromJson(json: TestPurchaseJson): TestPurchase {
+ return new TestPurchase();
+ }
+
+ toJson(): TestPurchaseJson {
+ return {};
+ }
+}
diff --git a/PurchaseConnector/models/validation_failure_data.ts b/PurchaseConnector/models/validation_failure_data.ts
new file mode 100644
index 00000000..03a0e048
--- /dev/null
+++ b/PurchaseConnector/models/validation_failure_data.ts
@@ -0,0 +1,25 @@
+export type ValidationFailureDataJson = {
+ status: number;
+ description: string;
+};
+
+export default class ValidationFailureData {
+ status: number;
+ description: string;
+
+ constructor(status: number, description: string) {
+ this.status = status;
+ this.description = description;
+ }
+
+ static fromJson(json: ValidationFailureDataJson): ValidationFailureData {
+ return new ValidationFailureData(json.status, json.description);
+ }
+
+ toJson(): ValidationFailureDataJson {
+ return {
+ status: this.status,
+ description: this.description,
+ };
+ }
+}
diff --git a/PurchaseConnector/utils/connector_callbacks.ts b/PurchaseConnector/utils/connector_callbacks.ts
new file mode 100644
index 00000000..6a0b5a50
--- /dev/null
+++ b/PurchaseConnector/utils/connector_callbacks.ts
@@ -0,0 +1,14 @@
+import { IosError, JVMThrowable } from "../models";
+
+// Type definition for a general-purpose listener.
+export type PurchaseConnectorListener = (data: any) => void;
+
+// Type definition for a listener which gets called when the `PurchaseConnectorImpl` receives purchase revenue validation info for iOS.
+export type OnReceivePurchaseRevenueValidationInfo = (validationInfo?: Map, error?: IosError) => void;
+
+// 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.
+export type OnResponse = (result?: Map) => void;
+
+// Invoked when a network exception occurs or a non 200/OK response is received from the server.
+export type OnFailure = (result: string, error?: JVMThrowable) => void;
\ No newline at end of file
diff --git a/README.md b/README.md
index 60091f8e..74c0bd25 100644
--- a/README.md
+++ b/README.md
@@ -12,8 +12,9 @@
### This plugin is built for
-- Android AppsFlyer SDK **v6.13.0**
-- iOS AppsFlyer SDK **v6.13.1**
+- Android AppsFlyer SDK **v6.15.1**
+- iOS AppsFlyer SDK **v6.15.1**
+- Tested with React-Native **v0.62.0** (older versions might be supported)
## ββ Breaking changes when updating to v6.x.xββ
@@ -46,24 +47,27 @@ And removed the following ones:
If you have used 1 of the removed APIs, please check the integration guide for the updated instructions.
- ---
+---
+
+## π Getting Started
- ## π Getting Started
- [Installation](/Docs/RN_Installation.md)
-- [***Expo*** Installation](/Docs/RN_ExpoInstallation.md)
+- [**_Expo_** Installation](/Docs/RN_ExpoInstallation.md)
- [Integration](/Docs/RN_Integration.md)
- [Test integration](/Docs/RN_Testing.md)
- [In-app events](/Docs/RN_InAppEvents.md)
- [Uninstall measurement](/Docs/RN_UninstallMeasurement.md)
- [Send consent for DMA compliance](/Docs/RN_CMP.md)
+- [Purchase Connector](/Docs/RN_PurchaseConnector.md)
## π Deep Linking
- [Integration](/Docs/RN_DeepLinkIntegrate.md)
-- [***Expo*** Integration](/Docs/RN_ExpoDeepLinkIntegration.md)
+- [**_Expo_** Integration](/Docs/RN_ExpoDeepLinkIntegration.md)
- [Unified Deep Link (UDL)](/Docs/RN_UnifiedDeepLink.md)
- [User Invite](/Docs/RN_UserInvite.md)
+
## π§ͺ Sample Apps
+
- [React-Native Sample App](/demos/appsflyer-react-native-app)
- [π Expo Sample App](https://github.com/AppsFlyerSDK/appsflyer-expo-sample-app)
### [API reference](/Docs/RN_API.md)
-
diff --git a/android/.project b/android/.project
index 3a1ae896..2e00bd16 100644
--- a/android/.project
+++ b/android/.project
@@ -1,15 +1,10 @@
- android_
+ react-native-appsflyer
Project android_ created by Buildship.
-
- org.eclipse.jdt.core.javabuilder
-
-
-
org.eclipse.buildship.core.gradleprojectbuilder
@@ -17,7 +12,6 @@
- org.eclipse.jdt.core.javanature
org.eclipse.buildship.core.gradleprojectnature
diff --git a/android/build.gradle b/android/build.gradle
index 47b22e87..b856e4dd 100755
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -13,11 +13,14 @@ buildscript {
dependencies {
classpath 'com.android.tools.build:gradle:7.2.2'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.10" // Kotlin plugin
}
}
-
apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android' // Apply Kotlin Android plugin
+
+def includeConnector = project.hasProperty('appsflyer.enable_purchase_connector') ? project.property('appsflyer.enable_purchase_connector').toBoolean() : false // Moved outside of android block
android {
compileSdkVersion safeExtGet('compileSdkVersion', 34)
@@ -30,9 +33,18 @@ android {
versionName "1.0"
ndk {
abiFilters "armeabi-v7a", "x86"
- 00
+ }
+
+ buildConfigField 'boolean', 'INCLUDE_CONNECTOR', includeConnector.toString()
+ }
+
+ sourceSets {
+ main {
+ java.srcDirs = ['src/main/java', 'src/main/kotlin'] // Add Kotlin source directory
+ java.srcDirs += includeConnector ? ['src/main/includeConnector'] : ['src/main/excludeConnector']
}
}
+
lintOptions {
warning 'InvalidPackage'
}
@@ -44,6 +56,9 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
+ kotlinOptions { // Configure Kotlin-specific options
+ jvmTarget = "17"
+ }
}
repositories {
@@ -52,7 +67,11 @@ repositories {
}
dependencies {
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:1.7.10" // Add Kotlin standard library
implementation "com.facebook.react:react-native:${safeExtGet('reactNativeVersion', '+')}"
- implementation "com.android.installreferrer:installreferrer:${safeExtGet('installReferrerVersion', '2.1')}"
api "com.appsflyer:af-android-sdk:${safeExtGet('appsflyerVersion', '6.15.1')}"
-}
+ implementation "com.android.installreferrer:installreferrer:${safeExtGet('installReferrerVersion', '2.2')}"
+ if (includeConnector){
+ implementation 'com.appsflyer:purchase-connector:2.0.1'
+ }
+}
\ No newline at end of file
diff --git a/android/src/main/excludeConnector/com/appsflyer/reactnative/PCAppsFlyerModule.java b/android/src/main/excludeConnector/com/appsflyer/reactnative/PCAppsFlyerModule.java
new file mode 100644
index 00000000..31a8e021
--- /dev/null
+++ b/android/src/main/excludeConnector/com/appsflyer/reactnative/PCAppsFlyerModule.java
@@ -0,0 +1,33 @@
+package com.appsflyer.reactnative;
+
+import android.app.Application;
+import android.content.Context;
+import android.util.Log;
+
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReactContext;
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
+import com.facebook.react.bridge.ReactMethod;
+
+public class PCAppsFlyerModule extends ReactContextBaseJavaModule {
+
+ public PCAppsFlyerModule(ReactApplicationContext reactContext) {
+ super(reactContext);
+ Log.d("AppsFlyer", "PurchaseConnector inclusion status: " + BuildConfig.INCLUDE_CONNECTOR);
+ }
+
+ @Override
+ public String getName() {
+ return "PCAppsFlyer";
+ }
+
+ @ReactMethod
+ public void addListener(String eventName) {
+ // Keep: Required for RN built in Event Emitter Calls.
+ }
+
+ @ReactMethod
+ public void removeListeners(Integer count) {
+ // Keep: Required for RN built in Event Emitter Calls.
+ }
+}
\ No newline at end of file
diff --git a/android/src/main/excludeConnector/com/appsflyer/reactnative/PCAppsFlyerPackage.java b/android/src/main/excludeConnector/com/appsflyer/reactnative/PCAppsFlyerPackage.java
new file mode 100644
index 00000000..18040c72
--- /dev/null
+++ b/android/src/main/excludeConnector/com/appsflyer/reactnative/PCAppsFlyerPackage.java
@@ -0,0 +1,32 @@
+package com.appsflyer.reactnative;
+
+import com.facebook.react.ReactPackage;
+import com.facebook.react.bridge.JavaScriptModule;
+import com.facebook.react.bridge.NativeModule;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.uimanager.ViewManager;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+public class PCAppsFlyerPackage implements ReactPackage {
+
+ public PCAppsFlyerPackage() {
+ }
+
+
+ public List> createJSModules() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List createNativeModules(ReactApplicationContext reactContext) {
+ return Arrays.asList(new PCAppsFlyerModule(reactContext));
+ }
+
+ @Override
+ public List createViewManagers(ReactApplicationContext reactContext) {
+ return Collections.emptyList();
+ }
+}
\ No newline at end of file
diff --git a/android/src/main/includeConnector/com/appsflyer/reactnative/ConnectorWrapper.kt b/android/src/main/includeConnector/com/appsflyer/reactnative/ConnectorWrapper.kt
new file mode 100644
index 00000000..853e0f8e
--- /dev/null
+++ b/android/src/main/includeConnector/com/appsflyer/reactnative/ConnectorWrapper.kt
@@ -0,0 +1,248 @@
+package com.appsflyer.reactnative;
+
+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
+
+/**
+ * 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/includeConnector/com/appsflyer/reactnative/MappedValidationResultListener.java b/android/src/main/includeConnector/com/appsflyer/reactnative/MappedValidationResultListener.java
new file mode 100644
index 00000000..99b3f7d6
--- /dev/null
+++ b/android/src/main/includeConnector/com/appsflyer/reactnative/MappedValidationResultListener.java
@@ -0,0 +1,9 @@
+package com.appsflyer.reactnative;
+
+import com.appsflyer.api.PurchaseClient;
+import java.util.Map;
+
+public interface MappedValidationResultListener extends PurchaseClient.ValidationResultListener