Release Updates
20 |
21 | - Starting with version `6.17.1` the plugin supports the Purchase Connector for validating and measuring Subscription and In-app purchase events. Integration guide can be found [here](https://github.com/AppsFlyerSDK/appsflyer-react-native-plugin/blob/master/Docs/RN_PurchaseConnector.md).
22 |
23 | - Starting with version `6.17.1` the TypeScript interfaces for Purchase Connector data sources have been simplified and are now **breaking changes**:
24 | - `PurchaseRevenueDataSource.purchaseRevenueAdditionalParametersForProducts()` function has been replaced with `additionalParameters` object
25 | - `PurchaseRevenueDataSourceStoreKit2.purchaseRevenueAdditionalParametersStoreKit2ForProducts()` function has been replaced with `additionalParameters` object
26 |
27 | - Starting with version `6.16.2`, `AppsFlyerConsent.forGDPRUser` and `AppsFlyerConsent.forNonGDPRUser` have been **deprecated**. Use the new `AppsFlyerConsent` constructor instead. See [Deprecation Notice](/Docs/RN_CMP.md#deprecation-notice).
28 |
29 | - Starting with version `6.15.1`, upgraded to targetSDKVersion 34, Java 17, and Gradle 8.7 in [AppsFlyer Android SDK v6.15.1](https://support.appsflyer.com/hc/en-us/articles/115001256006-AppsFlyer-Android-SDK-release-notes).
30 |
31 | - Starting with version `6.15.1`, iOS Minimum deployment target is set to 12.0.
32 |
33 | ---
34 |
35 | ## 🚀 Getting Started
36 |
37 | - [Installation](/Docs/RN_Installation.md)
38 | - [**_Expo_** Installation](/Docs/RN_ExpoInstallation.md)
39 | - [Integration](/Docs/RN_Integration.md)
40 | - [Test integration](/Docs/RN_Testing.md)
41 | - [In-app events](/Docs/RN_InAppEvents.md)
42 | - [Uninstall measurement](/Docs/RN_UninstallMeasurement.md)
43 | - [Send consent for DMA compliance](/Docs/RN_CMP.md)
44 | - [Purchase Connector](/Docs/RN_PurchaseConnector.md)
45 | ## 🔗 Deep Linking
46 | - [Integration](/Docs/RN_DeepLinkIntegrate.md)
47 | - [**_Expo_** Integration](/Docs/RN_ExpoDeepLinkIntegration.md)
48 | - [Unified Deep Link (UDL)](/Docs/RN_UnifiedDeepLink.md)
49 | - [User Invite](/Docs/RN_UserInvite.md)
50 |
51 | ## 🧪 Sample Apps
52 |
53 | - [React-Native Sample App](/demos/appsflyer-react-native-app)
54 | - [🆕 Expo Sample App](https://github.com/AppsFlyerSDK/appsflyer-expo-sample-app)
55 |
56 | ### [API reference](/Docs/RN_API.md)
57 |
--------------------------------------------------------------------------------
/.github/workflows/release-Production-workflow.yml:
--------------------------------------------------------------------------------
1 | name: Release plugin to production
2 |
3 | on:
4 | pull_request:
5 | types:
6 | - closed
7 | branches:
8 | - 'master'
9 | paths-ignore:
10 | - '**.md'
11 | - '**.yml'
12 | - 'demoes/**'
13 | - 'Docs/**'
14 | jobs:
15 | Deploy-To-Production:
16 | if: github.event.pull_request.merged == true
17 | runs-on: ubuntu-latest
18 | steps:
19 | - uses: actions/checkout@v3
20 | - name: Login to Github
21 | env:
22 | COMMIT_AUTHOR: ${{ secrets.CI_COMMIT_AUTHOR }}
23 | COMMIT_EMAIL: ${{ secrets.CI_COMMIT_EMAIL }}
24 | run: |
25 | git config --global user.name $COMMIT_AUTHOR
26 | git config --global user.email $COMMIT_EMAIL
27 |
28 | - uses: mdecoleman/pr-branch-name@1.2.0
29 | id: vars
30 | with:
31 | repo-token: ${{ secrets.GITHUB_TOKEN }}
32 | - name: Determine release tag and release branch
33 | run: |
34 | TAG=$(echo "${{ steps.vars.outputs.branch }}" | grep -Eo '[0-9].[0-9]+.[0-9]+')
35 | echo "PLUGIN_VERSION=$TAG" >> $GITHUB_ENV
36 | echo "RELEASE_BRANCH_NAME=${{ steps.vars.outputs.branch }}" >> $GITHUB_ENV
37 | echo "push new release >> $TAG"
38 |
39 | - name: "Create release"
40 | env:
41 | TAG: ${{env.PLUGIN_VERSION}}
42 | uses: "actions/github-script@v5"
43 | with:
44 | github-token: "${{ secrets.GITHUB_TOKEN }}"
45 | script: |
46 | try {
47 | await github.rest.repos.createRelease({
48 | draft: false,
49 | generate_release_notes: false,
50 | name: process.env.TAG,
51 | owner: context.repo.owner,
52 | prerelease: false,
53 | repo: context.repo.repo,
54 | tag_name: process.env.TAG
55 | });
56 | } catch (error) {
57 | core.setFailed(error.message);
58 | }
59 |
60 | - name: Push to NPM
61 | env:
62 | CI_NPM_TOKEN: ${{ secrets.CI_NPM_TOKEN }}
63 | run: |
64 | echo "//registry.npmjs.org/:_authToken=$CI_NPM_TOKEN" > ~/.npmrc
65 | npm publish
66 |
67 |
68 | - name: Generate and send slack report
69 | env:
70 | SLACK_TOKEN: ${{ secrets.CI_SLACK_TOKEN }}
71 | JIRA_TOKEN: ${{ secrets.CI_JIRA_TOKEN }}
72 | JIRA_FIXED_VERSION: "React Native SDK v${{env.PLUGIN_VERSION}}"
73 | RELEASE_BRACH_NAME: ${{env.RELEASE_BRANCH_NAME}}
74 | run: |
75 | chmod +x .github/workflows/scripts/releaseNotesGenerator.sh
76 | .github/workflows/scripts/releaseNotesGenerator.sh $JIRA_TOKEN "$JIRA_FIXED_VERSION"
77 | ios_sdk_version=$(cat react-native-appsflyer.podspec | grep '\'AppsFlyerFramework\' | grep -Eo '[0-9].[0-9]+.[0-9]+')
78 | android_sdk_version=$(cat android/build.gradle | grep 'com.appsflyer:af-android-sdk' | grep -Eo '[0-9].[0-9]+.[0-9]+')
79 | CHANGES=$(cat "$JIRA_FIXED_VERSION-releasenotes".txt)
80 | curl -X POST -H 'Content-type: application/json' --data '{"jira_fixed_version": "'"${{env.JIRA_FIXED_VERSION}}"'", "deploy_type": "Production", "install_tag": "latest", "git_branch": "'"$RELEASE_BRACH_NAME"'", "changes_and_fixes": "'"$CHANGES"'", "android_dependencie": "'"$android_sdk_version"'", "ios_dependencie": "'"$ios_sdk_version"'"}' "$SLACK_TOKEN"
81 |
--------------------------------------------------------------------------------
/Docs/RN_InAppEvents.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: In-App Events
3 | category: 5f9705393c689a065c409b23
4 | parentDoc: 645213236f53a00d4daa9230
5 | order: 5
6 | hidden: false
7 | ---
8 |
9 | ## In-App events
10 |
11 | In-App Events provide insight on what is happening in your app. It is recommended to take the time and define the events you want to measure to allow you to measure ROI (Return on Investment) and LTV (Lifetime Value).
12 |
13 | Recording in-app events is performed by calling logEvent with event name and value parameters. See In-App Events documentation for more details.
14 |
15 | **Note:** An In-App Event name must be no longer than 45 characters. Events names with more than 45 characters do not appear in the dashboard, but only in the raw Data, Pull and Push APIs.
16 | Find more info about recording events [here](https://dev.appsflyer.com/hc/docs/in-app-events-sdk).
17 |
18 | ## Send Event
19 |
20 | > 📘 Note
21 | >
22 | > For events with **revenue**, including in-app purchases, subscriptions, and ad revenue events, AppsFlyer customers with an ROI360 subscription should avoid using the `AFInAppEvents.REVENUE`(`af_revenue`) parameter in their in-app events. Doing so can result in duplicate revenue being reported. Instead, they should utilize the [ad revenue SDK API](https://dev.appsflyer.com/hc/docs/rn_api#logadrevenue).
23 |
24 | **`logEvent(eventName, eventValues, success, error)`**
25 |
26 | | parameter | type | description |
27 | | ----------- |----------|------------------------------------------ |
28 | | eventName | string | In-App Event name |
29 | | eventValues | json | The event values that are sent with the event
30 | | success | function | success callback |
31 | | error | function | error callback |
32 |
33 |
34 | *Example:*
35 | ```javascript
36 | const eventName = 'af_add_to_cart';
37 | const eventValues = {
38 | af_content_id: 'id123',
39 | af_currency: 'USD',
40 | af_revenue: '2',
41 | };
42 |
43 | appsFlyer.logEvent(
44 | eventName,
45 | eventValues,
46 | (res) => {
47 | console.log(res);
48 | },
49 | (err) => {
50 | console.error(err);
51 | }
52 | );
53 | ```
54 |
55 | ---
56 | ## In-app purchase validation
57 | Receipt validation is a secure mechanism whereby the payment platform (e.g. Apple or Google) validates that an in-app purchase indeed occurred as reported.
58 | Learn more [here](https://support.appsflyer.com/hc/en-us/articles/207032106-Receipt-validation-for-in-app-purchases).
59 |
60 | ❗Important❗ for iOS - set SandBox to ```true```
61 | ```appsFlyer.setUseReceiptValidationSandbox(true);```
62 |
63 | | parameter | type | description |
64 | | ---------- |----------|------------------ |
65 | | purchaseInfo | json | In-App Purchase parameters |
66 | | successC | function | success callback (generated link) |
67 | | errorC | function | error callback |
68 |
69 | *Example:*
70 | ```javascript
71 | let info = {
72 | publicKey: 'key',
73 | currency: 'biz',
74 | signature: 'sig',
75 | purchaseData: 'data',
76 | price: '123',
77 | productIdentifier: 'identifier',
78 | currency: 'USD',
79 | transactionId: '1000000614252747',
80 | additionalParameters: {'foo': 'bar'},
81 | };
82 | appsFlyer.validateAndLogInAppPurchase(info, res => console.log(res), err => console.log(err));
83 | ```
84 |
--------------------------------------------------------------------------------
/demos/appsflyer-react-native-app/android/app/src/debug/java/com/appsflyer.billing2/ReactNativeFlipper.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) Facebook, Inc. and its affiliates.
3 | *
4 | * This source code is licensed under the MIT license found in the LICENSE file in the root
5 | * directory of this source tree.
6 | */
7 | package com.appsflyerexample;
8 |
9 | import android.content.Context;
10 | import com.facebook.flipper.android.AndroidFlipperClient;
11 | import com.facebook.flipper.android.utils.FlipperUtils;
12 | import com.facebook.flipper.core.FlipperClient;
13 | import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin;
14 | import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin;
15 | import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin;
16 | import com.facebook.flipper.plugins.inspector.DescriptorMapping;
17 | import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin;
18 | import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor;
19 | import com.facebook.flipper.plugins.network.NetworkFlipperPlugin;
20 | import com.facebook.flipper.plugins.react.ReactFlipperPlugin;
21 | import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin;
22 | import com.facebook.react.ReactInstanceManager;
23 | import com.facebook.react.bridge.ReactContext;
24 | import com.facebook.react.modules.network.NetworkingModule;
25 | import okhttp3.OkHttpClient;
26 |
27 | public class ReactNativeFlipper {
28 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
29 | if (FlipperUtils.shouldEnableFlipper(context)) {
30 | final FlipperClient client = AndroidFlipperClient.getInstance(context);
31 |
32 | client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()));
33 | client.addPlugin(new ReactFlipperPlugin());
34 | client.addPlugin(new DatabasesFlipperPlugin(context));
35 | client.addPlugin(new SharedPreferencesFlipperPlugin(context));
36 | client.addPlugin(CrashReporterPlugin.getInstance());
37 |
38 | NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin();
39 | NetworkingModule.setCustomClientBuilder(
40 | new NetworkingModule.CustomClientBuilder() {
41 | @Override
42 | public void apply(OkHttpClient.Builder builder) {
43 | builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin));
44 | }
45 | });
46 | client.addPlugin(networkFlipperPlugin);
47 | client.start();
48 |
49 | // Fresco Plugin needs to ensure that ImagePipelineFactory is initialized
50 | // Hence we run if after all native modules have been initialized
51 | ReactContext reactContext = reactInstanceManager.getCurrentReactContext();
52 | if (reactContext == null) {
53 | reactInstanceManager.addReactInstanceEventListener(
54 | new ReactInstanceManager.ReactInstanceEventListener() {
55 | @Override
56 | public void onReactContextInitialized(ReactContext reactContext) {
57 | reactInstanceManager.removeReactInstanceEventListener(this);
58 | reactContext.runOnNativeModulesQueueThread(
59 | new Runnable() {
60 | @Override
61 | public void run() {
62 | client.addPlugin(new FrescoFlipperPlugin());
63 | }
64 | });
65 | }
66 | });
67 | } else {
68 | client.addPlugin(new FrescoFlipperPlugin());
69 | }
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/Docs/RN_ExpoDeepLinkIntegration.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Expo Deep linking integration
3 | category: 5f9705393c689a065c409b23
4 | parentDoc: 645213236f53a00d4daa9230
5 | order: 8
6 | hidden: false
7 | ---
8 |
9 | ## Getting started
10 |
11 | 
12 |
13 | ## Deep Linking Types
14 |
15 | 1. **Deferred Deep Linking** - Serving personalized content to new or former users, directly after the installation.
16 | 2. **Direct Deep Linking** - Directly serving personalized content to existing users, which already have the mobile app installed.
17 |
18 | **Unified deep linking (UDL)** - an API which enables you to send new and existing users to a specific in-app activity as soon as the app is opened.
19 |
20 | For more info please check out the [OneLink™ Deep Linking Guide](https://support.appsflyer.com/hc/en-us/articles/208874366-OneLink-Deep-Linking-Guide#Intro) and [developer guide](https://dev.appsflyer.com/hc/docs/dl_getting_started).
21 |
22 | ## Implementation for Expo
23 |
24 | 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).
25 |
26 | 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)**
27 |
28 | ## Full app.json example
29 |
30 | ```json
31 | {
32 | "expo": {
33 | "name": "expoAppsFlyer",
34 | "slug": "expoAppsFlyer",
35 | "version": "1.0.0",
36 | "orientation": "portrait",
37 | "icon": "./assets/atom.png",
38 | "plugins": [
39 | [
40 | "react-native-appsflyer",
41 | { "shouldUseStrictMode": true } // <<-- only for strict mode
42 | ]
43 | ],
44 | "splash": {
45 | "image": "./assets/splash.png",
46 | "resizeMode": "contain",
47 | "backgroundColor": "#ffffff"
48 | },
49 | "updates": {
50 | "fallbackToCacheTimeout": 0
51 | },
52 | "assetBundlePatterns": ["**/*"],
53 | "scheme": "my-own-scheme", // <<-- uri scheme as configured on AF dashboard
54 | "ios": {
55 | "supportsTablet": true,
56 | "bundleIdentifier": "com.appsflyer.expoaftest",
57 | "associatedDomains": ["applinks:expotest.onelink.me"] // <<-- important in order to use universal links
58 | },
59 | "android": {
60 | "adaptiveIcon": {
61 | "foregroundImage": "./assets/adaptive-icon.png",
62 | "backgroundColor": "#FFFFFF"
63 | },
64 | "package": "com.af.expotest",
65 | "intentFilters": [
66 | {
67 | "action": "VIEW",
68 | "data": [
69 | {
70 | "scheme": "https",
71 | "host": "expotest.onelink.me", // <<-- important for android App Links
72 | "pathPrefix": "/DvWi" // <<-- set your onelink template id
73 | }
74 | ],
75 | "category": ["BROWSABLE", "DEFAULT"]
76 | },
77 | {
78 | "action": "VIEW",
79 | "data": [
80 | {
81 | "scheme": "my-own-scheme" // <<-- uri scheme as configured on AF dashboard
82 | }
83 | ],
84 | "category": ["BROWSABLE", "DEFAULT"]
85 | }
86 | ]
87 | },
88 | "web": {
89 | "favicon": "./assets/favicon.png"
90 | }
91 | }
92 | }
93 | ```
94 |
--------------------------------------------------------------------------------
/ios/RNAppsFlyer.h:
--------------------------------------------------------------------------------
1 |
2 | #if __has_include() //ver >= 0.40
3 | #import
4 | #import
5 | #import
6 | #else //ver < 0.40
7 | #import "RCTBridgeModule.h"
8 | #import "RCTEventDispatcher.h"
9 | #endif
10 |
11 | #import "AppsFlyerAttribution.h"
12 | #import
13 | #if __has_include() // from Pod
14 | #import
15 | #else
16 | #import "AppsFlyerLib.h"
17 | #endif
18 |
19 |
20 | @interface RNAppsFlyer: RCTEventEmitter
21 | @property (readwrite, nonatomic) BOOL isManualStart;
22 | @end
23 |
24 |
25 | static NSString *const kAppsFlyerPluginVersion = @"6.17.7";
26 | static NSString *const NO_DEVKEY_FOUND = @"No 'devKey' found or its empty";
27 | static NSString *const NO_APPID_FOUND = @"No 'appId' found or its empty";
28 | static NSString *const NO_EVENT_NAME_FOUND = @"No 'eventName' found or its empty";
29 | static NSString *const EMPTY_OR_CORRUPTED_LIST = @"No arguments found or list is corrupted";
30 | static NSString *const AF_SUCCESS = @"Success";
31 | static NSString *const INVALID_URI = @"Invalid URI";
32 | static NSString *const IOS_14_ONLY = @"Feature only supported on iOS 14 and above";
33 |
34 | // Appsflyer JS objects
35 | #define afDevKey @"devKey"
36 | #define afAppId @"appId"
37 | #define afIsDebug @"isDebug"
38 | #define timeToWaitForATTUserAuthorization @"timeToWaitForATTUserAuthorization"
39 |
40 | #define afEmailsCryptType @"emailsCryptType"
41 | #define afEmails @"emails"
42 |
43 | // Appsflyer native objects
44 | #define afConversionData @"onInstallConversionDataListener"
45 | #define afOnInstallConversionData @"onInstallConversionData"
46 | #define afSuccess @"success"
47 | #define afFailure @"failure"
48 | #define afOnAttributionFailure @"onAttributionFailure"
49 | #define afOnAppOpenAttribution @"onAppOpenAttribution"
50 | #define afOnInstallConversionFailure @"onInstallConversionFailure"
51 | #define afOnInstallConversionDataLoaded @"onInstallConversionDataLoaded"
52 | #define afDeepLink @"onDeepLinkListener"
53 | #define afOnDeepLinking @"onDeepLinking"
54 | #define afOnValidationResult @"onValidationResult"
55 |
56 | // User Invites, Cross Promotion
57 | #define afCpAppID @"crossPromotedAppId"
58 | #define afUiChannel @"channel"
59 | #define afUiCampaign @"campaign"
60 | #define afUiRefName @"referrerName"
61 | #define afUiImageUrl @"referrerImageUrl"
62 | #define afUiCustomerID @"customerID"
63 | #define afUiBaseDeepLink @"baseDeepLink"
64 |
65 | //RECEIPT VALIDATION
66 | #define afProductIdentifier @"productIdentifier"
67 | #define afTransactionId @"transactionId"
68 | #define afPrice @"price"
69 | #define afCurrency @"currency"
70 | #define afAdditionalParameters @"additionalParameters"
71 | static NSString *const NO_PARAMETERS_ERROR = @"No purchase parameters found";
72 | static NSString *const VALIDATE_SUCCESS = @"In-App Purchase Validation success";
73 | static NSString *const VALIDATE_FAILED = @"In-App Purchase Validation failed with error: ";
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/demos/appsflyer-react-native-app/android/app/src/main/java/com/appsflyerexample/MainApplication.java:
--------------------------------------------------------------------------------
1 | package com.appsflyerexample;
2 |
3 | import android.app.Application;
4 | import android.content.Context;
5 |
6 | import com.appsflyer.reactnative.RNAppsFlyerPackage;
7 | import com.appsflyer.reactnative.PCAppsFlyerPackage;
8 | import com.facebook.react.PackageList;
9 | import com.facebook.react.ReactApplication;
10 | import com.facebook.react.ReactInstanceManager;
11 | import com.facebook.react.ReactNativeHost;
12 | import com.facebook.react.ReactPackage;
13 | import com.facebook.soloader.SoLoader;
14 | import java.lang.reflect.InvocationTargetException;
15 | import java.util.List;
16 | import android.content.BroadcastReceiver;
17 | import android.content.Intent;
18 | import android.content.IntentFilter;
19 | import android.os.Build;
20 | import org.jetbrains.annotations.Nullable;
21 | import android.content.Context;
22 |
23 | public class MainApplication extends Application implements ReactApplication {
24 |
25 | private final ReactNativeHost mReactNativeHost =
26 | new ReactNativeHost(this) {
27 | @Override
28 | public boolean getUseDeveloperSupport() {
29 | return BuildConfig.DEBUG;
30 | }
31 |
32 | @Override
33 | protected List getPackages() {
34 | @SuppressWarnings("UnnecessaryLocalVariable")
35 | List packages = new PackageList(this).getPackages();
36 | // Packages that cannot be autolinked yet can be added manually here, for example:
37 | // packages.add(new MyReactNativePackage());
38 | return packages;
39 | }
40 |
41 | @Override
42 | protected String getJSMainModuleName() {
43 | return "index";
44 | }
45 | };
46 |
47 | @Override
48 | public ReactNativeHost getReactNativeHost() {
49 | return mReactNativeHost;
50 | }
51 |
52 | @Override
53 | public Intent registerReceiver(@Nullable BroadcastReceiver receiver, IntentFilter filter) {
54 | if (Build.VERSION.SDK_INT >= 34 && getApplicationInfo().targetSdkVersion >= 34) {
55 | return super.registerReceiver(receiver, filter, Context.RECEIVER_EXPORTED);
56 | } else {
57 | return super.registerReceiver(receiver, filter);
58 | }
59 | }
60 |
61 | @Override
62 | public void onCreate() {
63 | super.onCreate();
64 | SoLoader.init(this, /* native exopackage */ false);
65 | initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
66 | }
67 |
68 | /**
69 | * Loads Flipper in React Native templates. Call this in the onCreate method with something like
70 | * initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
71 | *
72 | * @param context
73 | * @param reactInstanceManager
74 | */
75 | private static void initializeFlipper(
76 | Context context, ReactInstanceManager reactInstanceManager) {
77 | if (BuildConfig.DEBUG) {
78 | try {
79 | /*
80 | We use reflection here to pick up the class that initializes Flipper,
81 | since Flipper library is not available in release mode
82 | */
83 | Class> aClass = Class.forName("com.appsflyerexample.ReactNativeFlipper");
84 | aClass
85 | .getMethod("initializeFlipper", Context.class, ReactInstanceManager.class)
86 | .invoke(null, context, reactInstanceManager);
87 | } catch (ClassNotFoundException e) {
88 | e.printStackTrace();
89 | } catch (NoSuchMethodException e) {
90 | e.printStackTrace();
91 | } catch (IllegalAccessException e) {
92 | e.printStackTrace();
93 | } catch (InvocationTargetException e) {
94 | e.printStackTrace();
95 | }
96 | }
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerConstants.java:
--------------------------------------------------------------------------------
1 | package com.appsflyer.reactnative;
2 |
3 | /**
4 | * Created by maxim on 11/17/16.
5 | */
6 |
7 | public class RNAppsFlyerConstants {
8 |
9 | final static String PLUGIN_VERSION = "6.17.7";
10 | final static String NO_DEVKEY_FOUND = "No 'devKey' found or its empty";
11 | final static String UNKNOWN_ERROR = "AF Unknown Error";
12 | final static String SUCCESS = "Success";
13 | final static String NO_EVENT_NAME_FOUND = "No 'eventName' found or its empty";
14 | final static String EMPTY_OR_CORRUPTED_LIST = "No arguments found or list is corrupted";
15 | final static String INVALID_URI = "Passed string is not a valid URI";
16 |
17 |
18 | final static String afIsDebug = "isDebug";
19 | final static String afDevKey = "devKey";
20 | final static String afEmailsCryptType = "emailsCryptType";
21 | final static String afEmails = "emails";
22 |
23 | final static String afConversionData = "onInstallConversionDataListener";
24 | final static String afDeepLink = "onDeepLinkListener";
25 |
26 | final static String afSuccess = "success";
27 | final static String afFailure = "failure";
28 | final static String afOnAttributionFailure = "onAttributionFailure";
29 | final static String afOnAppOpenAttribution = "onAppOpenAttribution";
30 | final static String afOnInstallConversionFailure = "onInstallConversionFailure";
31 | final static String afOnInstallConversionDataLoaded = "onInstallConversionDataLoaded";
32 | final static String afOnDeepLinking = "onDeepLinking";
33 | final static String afOnValidationResult = "onValidationResult";
34 |
35 | final static String INVITE_FAIL = "Could not create invite link";
36 | final static String INVITE_CHANNEL = "channel";
37 | final static String INVITE_CAMPAIGN = "campaign";
38 | final static String INVITE_REFERRER = "referrerName";
39 | final static String INVITE_IMAGEURL = "referreImageURL";
40 | final static String INVITE_CUSTOMERID = "customerID";
41 | final static String INVITE_DEEPLINK = "baseDeepLink";
42 | final static String PROMOTE_ID = "promotedAppId";
43 | final static String INVITE_BRAND_DOMAIN = "brandDomain";
44 |
45 | //RECEIPT VALIDATION
46 | final static String PUBLIC_KEY = "publicKey";
47 | final static String SIGNATURE = "signature";
48 | final static String PURCHASE_DATA = "purchaseData";
49 | final static String PRICE = "price";
50 | final static String CURRENCY = "currency";
51 | final static String ADDITIONAL_PARAMETERS = "additionalParameters";
52 | final static String NO_PARAMETERS_ERROR = "Please provide purchase parameters";
53 | final static String VALIDATE_SUCCESS = "In-App Purchase Validation success";
54 | final static String VALIDATE_FAILED = "In-App Purchase Validation failed with error: ";
55 |
56 | final static String MONETIZATION_NETWORK = "monetizationNetwork";
57 | final static String CURRENCY_ISO4217_CODE = "currencyIso4217Code";
58 | final static String AF_REVENUE = "revenue";
59 | final static String AF_MEDIATION_NETWORK = "mediationNetwork";
60 | final static String AF_ADDITIONAL_PARAMETERS = "additionalParameters";
61 |
62 | //Purchase Connector
63 | final static String EVENT_SUBSCRIPTION_VALIDATION_SUCCESS = "subscriptionValidationSuccess";
64 | final static String EVENT_SUBSCRIPTION_VALIDATION_FAILURE = "subscriptionValidationFailure";
65 | final static String EVENT_IN_APP_PURCHASE_VALIDATION_SUCCESS = "inAppPurchaseValidationSuccess";
66 | final static String EVENT_IN_APP_PURCHASE_VALIDATION_FAILURE = "inAppPurchaseValidationFailure";
67 | final static String ENABLE_MODULE_MESSAGE = "Please set appsflyer.enable_purchase_connector to true in your gradle.properties file.";
68 | }
69 |
70 |
--------------------------------------------------------------------------------
/demos/appsflyer-react-native-app/ios/AppsFlyerExample.xcodeproj/xcshareddata/xcschemes/AppsFlyerExample.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
53 |
55 |
61 |
62 |
63 |
64 |
70 |
72 |
78 |
79 |
80 |
81 |
83 |
84 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/PurchaseConnector/models/product_purchase.ts:
--------------------------------------------------------------------------------
1 | export type ProductPurchaseArgs = {
2 | kind: string;
3 | purchaseTimeMillis: string;
4 | purchaseState: number;
5 | consumptionState: number;
6 | developerPayload: string;
7 | orderId: string;
8 | purchaseType: number;
9 | acknowledgementState: number;
10 | purchaseToken: string;
11 | productId: string;
12 | quantity: number;
13 | obfuscatedExternalAccountId: string;
14 | obfuscatedExternalProfileId: string;
15 | regionCode: string;
16 | };
17 |
18 | export class ProductPurchase {
19 | kind: string;
20 | purchaseTimeMillis: string;
21 | purchaseState: number;
22 | consumptionState: number;
23 | developerPayload: string;
24 | orderId: string;
25 | purchaseType: number;
26 | acknowledgementState: number;
27 | purchaseToken: string;
28 | productId: string;
29 | quantity: number;
30 | obfuscatedExternalAccountId: string;
31 | obfuscatedExternalProfileId: string;
32 | regionCode: string;
33 |
34 | constructor(args: ProductPurchaseArgs) {
35 | this.kind = args.kind;
36 | this.purchaseTimeMillis = args.purchaseTimeMillis;
37 | this.purchaseState = args.purchaseState;
38 | this.consumptionState = args.consumptionState;
39 | this.developerPayload = args.developerPayload;
40 | this.orderId = args.orderId;
41 | this.purchaseType = args.purchaseType;
42 | this.acknowledgementState = args.acknowledgementState;
43 | this.purchaseToken = args.purchaseToken;
44 | this.productId = args.productId;
45 | this.quantity = args.quantity;
46 | this.obfuscatedExternalAccountId = args.obfuscatedExternalAccountId;
47 | this.obfuscatedExternalProfileId = args.obfuscatedExternalProfileId;
48 | this.regionCode = args.regionCode;
49 | }
50 |
51 | toJson(): Record {
52 | return {
53 | kind: this.kind,
54 | purchaseTimeMillis: this.purchaseTimeMillis,
55 | purchaseState: this.purchaseState,
56 | consumptionState: this.consumptionState,
57 | developerPayload: this.developerPayload,
58 | orderId: this.orderId,
59 | purchaseType: this.purchaseType,
60 | acknowledgementState: this.acknowledgementState,
61 | purchaseToken: this.purchaseToken,
62 | productId: this.productId,
63 | quantity: this.quantity,
64 | obfuscatedExternalAccountId: this.obfuscatedExternalAccountId,
65 | obfuscatedExternalProfileId: this.obfuscatedExternalProfileId,
66 | regionCode: this.regionCode,
67 | };
68 | }
69 |
70 | static fromJson(json: any): ProductPurchase {
71 | return new ProductPurchase({
72 | kind: json.kind as string,
73 | purchaseTimeMillis: json.purchaseTimeMillis as string,
74 | purchaseState: json.purchaseState as number,
75 | consumptionState: json.consumptionState as number,
76 | developerPayload: json.developerPayload as string,
77 | orderId: json.orderId as string,
78 | purchaseType: json.purchaseType as number,
79 | acknowledgementState: json.acknowledgementState as number,
80 | purchaseToken: json.purchaseToken as string,
81 | productId: json.productId as string,
82 | quantity: json.quantity as number,
83 | obfuscatedExternalAccountId: json.obfuscatedExternalAccountId as string,
84 | obfuscatedExternalProfileId: json.obfuscatedExternalProfileId as string,
85 | regionCode: json.regionCode as string,
86 | });
87 | }
88 | }
89 |
90 | /**
91 | * // Usage:
92 | * // To convert to JSON:
93 | * const productPurchaseInstance = new ProductPurchase({ ...args });
94 | * const json = productPurchaseInstance.toJson();
95 | *
96 | * // To convert from JSON:
97 | * const json = {...json };
98 | * const productPurchaseInstance = ProductPurchase.fromJson(json);
99 | */
100 |
--------------------------------------------------------------------------------
/PurchaseConnector/models/subscription_purchase.ts:
--------------------------------------------------------------------------------
1 | import { CanceledStateContext } from "./canceled_state_context";
2 | import { ExternalAccountIdentifiers } from "./external_account_identifiers";
3 | import { PausedStateContext } from "./paused_state_context";
4 | import { SubscribeWithGoogleInfo } from "./subscribe_with_google_info";
5 | import { SubscriptionPurchaseLineItem } from "./subscription_purchase_line_item";
6 | import { TestPurchase } from "./test_purchase";
7 |
8 | type SubscriptionPurchaseArgs = {
9 | acknowledgementState: string;
10 | canceledStateContext?: CanceledStateContext;
11 | externalAccountIdentifiers?: ExternalAccountIdentifiers;
12 | kind: string;
13 | latestOrderId: string;
14 | lineItems: SubscriptionPurchaseLineItem[];
15 | linkedPurchaseToken?: string;
16 | pausedStateContext?: PausedStateContext;
17 | regionCode: string;
18 | startTime: string;
19 | subscribeWithGoogleInfo?: SubscribeWithGoogleInfo;
20 | subscriptionState: string;
21 | testPurchase?: TestPurchase;
22 | };
23 |
24 | export class SubscriptionPurchase {
25 | acknowledgementState: string;
26 | canceledStateContext?: CanceledStateContext;
27 | externalAccountIdentifiers?: ExternalAccountIdentifiers;
28 | kind: string;
29 | latestOrderId: string;
30 | lineItems: SubscriptionPurchaseLineItem[];
31 | linkedPurchaseToken?: string;
32 | pausedStateContext?: PausedStateContext;
33 | regionCode: string;
34 | startTime: string;
35 | subscribeWithGoogleInfo?: SubscribeWithGoogleInfo;
36 | subscriptionState: string;
37 | testPurchase?: TestPurchase;
38 |
39 | constructor(args: SubscriptionPurchaseArgs) {
40 | this.acknowledgementState = args.acknowledgementState;
41 | this.canceledStateContext = args.canceledStateContext;
42 | this.externalAccountIdentifiers = args.externalAccountIdentifiers;
43 | this.kind = args.kind;
44 | this.latestOrderId = args.latestOrderId;
45 | this.lineItems = args.lineItems;
46 | this.linkedPurchaseToken = args.linkedPurchaseToken;
47 | this.pausedStateContext = args.pausedStateContext;
48 | this.regionCode = args.regionCode;
49 | this.startTime = args.startTime;
50 | this.subscribeWithGoogleInfo = args.subscribeWithGoogleInfo;
51 | this.subscriptionState = args.subscriptionState;
52 | this.testPurchase = args.testPurchase;
53 | }
54 |
55 | static fromJson(json: any): SubscriptionPurchase {
56 | return new SubscriptionPurchase({
57 | acknowledgementState: json.acknowledgementState as string,
58 | canceledStateContext: json.canceledStateContext,
59 | externalAccountIdentifiers: json.externalAccountIdentifiers,
60 | kind: json.kind as string,
61 | latestOrderId: json.latestOrderId as string,
62 | lineItems: json.lineItems,
63 | linkedPurchaseToken: json.linkedPurchaseToken as string,
64 | pausedStateContext: json.pausedStateContext,
65 | regionCode: json.regionCode as string,
66 | startTime: json.startTime as string,
67 | subscribeWithGoogleInfo: json.subscribeWithGoogleInfo,
68 | subscriptionState: json.subscriptionState as string,
69 | testPurchase: json.testPurchase,
70 | });
71 | }
72 |
73 | toJson(): Record {
74 | return {
75 | acknowledgementState: this.acknowledgementState,
76 | canceledStateContext: this.canceledStateContext,
77 | externalAccountIdentifiers: this.externalAccountIdentifiers,
78 | kind: this.kind,
79 | latestOrderId: this.latestOrderId,
80 | lineItems: this.lineItems,
81 | linkedPurchaseToken: this.linkedPurchaseToken,
82 | pausedStateContext: this.pausedStateContext,
83 | regionCode: this.regionCode,
84 | startTime: this.startTime,
85 | subscribeWithGoogleInfo: this.subscribeWithGoogleInfo,
86 | subscriptionState: this.subscriptionState,
87 | testPurchase: this.testPurchase,
88 | };
89 | }
90 | }
91 |
92 |
--------------------------------------------------------------------------------
/ios/AppsFlyerAttribution.m:
--------------------------------------------------------------------------------
1 | //
2 | // AppsFlyerAttribution.m
3 | // react-native-appsflyer
4 | //
5 | // Created by Amit Kremer on 11/02/2021.
6 | //
7 |
8 | #import
9 | #import "AppsFlyerAttribution.h"
10 | #import
11 |
12 | @interface AppsFlyerAttribution ()
13 | @property NSUserActivity * userActivity;
14 | @property NSURL * url;
15 | @property NSString * sourceApplication;
16 | @property NSDictionary * options;
17 |
18 | @end
19 |
20 | @implementation AppsFlyerAttribution
21 |
22 | + (instancetype)shared {
23 | static AppsFlyerAttribution *shared = nil;
24 | static dispatch_once_t onceToken;
25 | dispatch_once(&onceToken, ^{
26 | shared = [[self alloc] init];
27 | });
28 | return shared;
29 | }
30 |
31 | - (id)init {
32 | if (self = [super init]) {
33 | _url = nil;
34 | _userActivity = nil;
35 | _sourceApplication = nil;
36 | _options = nil;
37 | _RNAFBridgeReady = NO;
38 |
39 | [[NSNotificationCenter defaultCenter] addObserver:self
40 | selector:@selector(receiveBridgeReadyNotification:)
41 | name:RNAFBridgeInitializedNotification
42 | object:nil];
43 | }
44 | return self;
45 | }
46 |
47 | #pragma mark - AppDelegate methods
48 |
49 | - (void)continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^_Nullable)(NSArray * _Nullable))restorationHandler{
50 | if([self RNAFBridgeReady] == YES){
51 | [[AppsFlyerLib shared] continueUserActivity:userActivity restorationHandler:restorationHandler];
52 | }else{
53 | [self setUserActivity:userActivity];
54 | }
55 | }
56 |
57 | - (void)handleOpenUrl:(NSURL *)url options:(NSDictionary *)options{
58 | if([self RNAFBridgeReady] == YES){
59 | [[AppsFlyerLib shared] handleOpenUrl:url options:options];
60 | }else{
61 | [self setUrl:url];
62 | [self setOptions:options];
63 | }
64 | }
65 |
66 | - (void)handleOpenUrl:(NSURL *)url sourceApplication:(NSString*)sourceApplication annotation:(id)annotation{
67 | if([self RNAFBridgeReady] == YES){
68 | [[AppsFlyerLib shared] handleOpenURL:url sourceApplication:sourceApplication withAnnotation:annotation];
69 | }else{
70 | [self setUrl:url];
71 | [self setSourceApplication:sourceApplication];
72 | }
73 | }
74 |
75 | #pragma mark - Bridge initialized notification
76 |
77 | - (void)receiveBridgeReadyNotification:(NSNotification *)notification {
78 | // RD-69546
79 | // start - We added this code because sometimes the SDK automatically resolves deeplinks on `applicationDidFinishLaunching`, and then when calling `continueUserActivity` on the same deeplink
80 | // it skips them.
81 | id AppsFlyer = [AppsFlyerLib shared];
82 | SEL setSkipNextUniversalLinkAttribution = NSSelectorFromString(@"setSkipNextUniversalLinkAttribution:");
83 | if ([AppsFlyer respondsToSelector:setSkipNextUniversalLinkAttribution]) {
84 | ((void ( *) (id, SEL, BOOL))[AppsFlyer methodForSelector:setSkipNextUniversalLinkAttribution])(AppsFlyer, setSkipNextUniversalLinkAttribution, NO);
85 | }
86 | // end
87 |
88 | if([self url] && [self sourceApplication]){
89 | [[AppsFlyerLib shared] handleOpenURL:[self url] sourceApplication:[self sourceApplication] withAnnotation:nil];
90 | [self setUrl:nil];
91 | [self setSourceApplication:nil];
92 | }else if([self url] && [self options]){
93 | [[AppsFlyerLib shared] handleOpenUrl:[self url] options:[self options]];
94 | [self setUrl:nil];
95 | [self setOptions:nil];
96 | }else if([self userActivity]){
97 | [[AppsFlyerLib shared] continueUserActivity:[self userActivity] restorationHandler:nil];
98 | [self setUserActivity:nil];
99 | }
100 | }
101 | @end
102 |
--------------------------------------------------------------------------------
/demos/appsflyer-react-native-app/components/Cart.js:
--------------------------------------------------------------------------------
1 | /* @flow weak */
2 | import React from 'react';
3 | import {View, StyleSheet, ScrollView, Alert, Platform} from 'react-native';
4 | import {ListItem, Avatar, Button} from 'react-native-elements';
5 | import {
6 | getSubscriptions,
7 | requestPurchase,
8 | requestSubscription,
9 | RequestPurchase,
10 | finishTransaction,
11 | } from 'react-native-iap';
12 |
13 | const Cart = ({route, navigation}) => {
14 | const {productList, removeProductFromCart, checkout} = route.params;
15 |
16 | /*
17 | // Added methods
18 | const purchase = async (sku: string) => {
19 | try {
20 | let purchaseParams: RequestPurchase = {
21 | sku,
22 | andDangerouslyFinishTransactionAutomaticallyIOS: false,
23 | };
24 | if (Platform.OS === 'android') {
25 | purchaseParams = {skus: [sku]};
26 | }
27 | await requestPurchase(purchaseParams);
28 | } catch (err) {
29 | console.warn(err.code, err.message);
30 | }
31 | };
32 |
33 | const subscribe = async (sku, offerToken) => {
34 | try {
35 | const offerDetails = await getSubscriptions({skus: [sku]});
36 | const subscriptionOffer = offerDetails.find(
37 | offer => offer.productId === sku,
38 | );
39 |
40 | // Check if offer details exist for the sku
41 | if (Platform.OS == 'android') {
42 | if (
43 | !subscriptionOffer ||
44 | !subscriptionOffer.subscriptionOfferDetails ||
45 | subscriptionOffer.subscriptionOfferDetails.length === 0
46 | ) {
47 | throw new Error(
48 | 'Subscription offer details not found for sku: ' + sku,
49 | );
50 | }
51 | const offerToken =
52 | subscriptionOffer.subscriptionOfferDetails[0].offerToken;
53 | }
54 |
55 | await requestSubscription({
56 | sku,
57 | ...(offerToken && {subscriptionOffers: [{sku, offerToken}]}),
58 | });
59 | } catch (err) {
60 | console.warn(err.code, err.message);
61 | }
62 | };
63 | */
64 |
65 | const handleRemove = product => {
66 | removeProductFromCart(product);
67 | navigation.goBack();
68 | };
69 |
70 | const handleCheckout = () => {
71 | if (productList.length !== 0) {
72 | checkout();
73 | navigation.goBack();
74 | } else {
75 | Alert.alert(
76 | 'Cart Empty',
77 | 'The cart is empty.',
78 | [
79 | {
80 | text: 'OK',
81 | onPress: () => console.log('OK Pressed'),
82 | style: 'cancel',
83 | },
84 | ],
85 | {
86 | cancelable: true,
87 | onDismiss: () => console.log('Alert dismissed'),
88 | },
89 | );
90 | }
91 | };
92 |
93 | return (
94 |
95 |
96 | {productList.map((product, index) => (
97 | handleRemove(product)}
105 | />
106 | }>
107 |
108 |
109 | {product.name}
110 | {`${product.price} USD`}
111 |
112 |
113 | ))}
114 |
115 |
120 |
121 | );
122 | };
123 |
124 | export default Cart;
125 |
126 | const styles = StyleSheet.create({
127 | container: {
128 | flex: 1,
129 | },
130 | checkoutButton: {
131 | height: 75,
132 | },
133 | });
134 |
--------------------------------------------------------------------------------
/demos/appsflyer-react-native-app/ios/AppsFlyerExample/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
24 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/PurchaseConnector/models/canceled_state_context.ts:
--------------------------------------------------------------------------------
1 | export class CanceledStateContext {
2 | developerInitiatedCancellation?: DeveloperInitiatedCancellation;
3 | replacementCancellation?: ReplacementCancellation;
4 | systemInitiatedCancellation?: SystemInitiatedCancellation;
5 | userInitiatedCancellation?: UserInitiatedCancellation;
6 |
7 | constructor(
8 | developerInitiatedCancellation?: DeveloperInitiatedCancellation,
9 | replacementCancellation?: ReplacementCancellation,
10 | systemInitiatedCancellation?: SystemInitiatedCancellation,
11 | userInitiatedCancellation?: UserInitiatedCancellation
12 | ) {
13 | this.developerInitiatedCancellation = developerInitiatedCancellation;
14 | this.replacementCancellation = replacementCancellation;
15 | this.systemInitiatedCancellation = systemInitiatedCancellation;
16 | this.userInitiatedCancellation = userInitiatedCancellation;
17 | }
18 |
19 | static fromJson(json: any): CanceledStateContext {
20 | return new CanceledStateContext(
21 | json.developerInitiatedCancellation != null
22 | ? DeveloperInitiatedCancellation.fromJson(
23 | json.developerInitiatedCancellation
24 | )
25 | : undefined,
26 | json.replacementCancellation != null
27 | ? ReplacementCancellation.fromJson(json.replacementCancellation)
28 | : undefined,
29 | json.systemInitiatedCancellation != null
30 | ? SystemInitiatedCancellation.fromJson(json.systemInitiatedCancellation)
31 | : undefined,
32 | json.userInitiatedCancellation != null
33 | ? UserInitiatedCancellation.fromJson(json.userInitiatedCancellation)
34 | : undefined
35 | );
36 | }
37 |
38 | toJson(): Record {
39 | return {
40 | developerInitiatedCancellation:
41 | this.developerInitiatedCancellation?.toJson(),
42 | replacementCancellation: this.replacementCancellation?.toJson(),
43 | systemInitiatedCancellation: this.systemInitiatedCancellation?.toJson(),
44 | userInitiatedCancellation: this.userInitiatedCancellation?.toJson(),
45 | };
46 | }
47 | }
48 |
49 | /**
50 | * TODO: Need to check each state context further...
51 | */
52 | class DeveloperInitiatedCancellation {
53 | constructor() {}
54 |
55 | static fromJson(json: any): DeveloperInitiatedCancellation {
56 | // Here you would implement the conversion from JSON to DeveloperInitiatedCancellation instance
57 | return new DeveloperInitiatedCancellation();
58 | }
59 |
60 | toJson(): Record {
61 | // Here you would implement the conversion from DeveloperInitiatedCancellation instance to JSON
62 | return {};
63 | }
64 | }
65 |
66 | class ReplacementCancellation {
67 | constructor() {}
68 |
69 | static fromJson(json: any): ReplacementCancellation {
70 | // Here you would implement the conversion from JSON to ReplacementCancellation instance
71 | return new ReplacementCancellation();
72 | }
73 |
74 | toJson(): Record {
75 | return {};
76 | }
77 | }
78 |
79 | class SystemInitiatedCancellation {
80 | constructor() {}
81 |
82 | static fromJson(json: any): SystemInitiatedCancellation {
83 | // Here you would implement the conversion from JSON to SystemInitiatedCancellation instance
84 | return new SystemInitiatedCancellation();
85 | }
86 |
87 | toJson(): Record {
88 | // Here you would implement the conversion from SystemInitiatedCancellation instance to JSON
89 | return {};
90 | }
91 | }
92 |
93 | class UserInitiatedCancellation {
94 | cancelSurveyResult?: CancelSurveyResult; // Made optional as per Dart's CancelSurveyResult? declaration
95 | cancelTime: string;
96 |
97 | constructor(
98 | cancelSurveyResult: CancelSurveyResult | undefined,
99 | cancelTime: string
100 | ) {
101 | this.cancelSurveyResult = cancelSurveyResult;
102 | this.cancelTime = cancelTime;
103 | }
104 |
105 | static fromJson(json: any): UserInitiatedCancellation {
106 | return new UserInitiatedCancellation(
107 | json.cancelSurveyResult != null
108 | ? CancelSurveyResult.fromJson(json.cancelSurveyResult)
109 | : undefined,
110 | json.cancelTime
111 | );
112 | }
113 |
114 | toJson(): Record {
115 | return {
116 | cancelSurveyResult: this.cancelSurveyResult?.toJson(),
117 | cancelTime: this.cancelTime,
118 | };
119 | }
120 | }
121 |
122 | class CancelSurveyResult {
123 | reason: string;
124 | reasonUserInput: string;
125 |
126 | constructor(reason: string, reasonUserInput: string) {
127 | this.reason = reason;
128 | this.reasonUserInput = reasonUserInput;
129 | }
130 |
131 | static fromJson(json: any): CancelSurveyResult {
132 | return new CancelSurveyResult(json.reason, json.reasonUserInput);
133 | }
134 |
135 | toJson(): Record {
136 | return {
137 | reason: this.reason,
138 | reasonUserInput: this.reasonUserInput,
139 | };
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/Docs/RN_ExpoInstallation.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Expo Installation
3 | category: 5f9705393c689a065c409b23
4 | parentDoc: 645213236f53a00d4daa9230
5 | order: 2
6 | hidden: false
7 | ---
8 |
9 | ## Install AppsFlyer in an Expo managed project
10 | 1. Install `expo-dev-client`. You can read more about expo development builds [here](https://docs.expo.dev/development/introduction/):
11 | ```
12 | expo install expo-dev-client
13 | ```
14 |
15 | 2. Install react-native-appsflyer:
16 | ```
17 | expo install react-native-appsflyer
18 | ```
19 |
20 | 3. Add `react-native-appsflyer` into the `plugins` array inside the `app.json` file of your app:
21 | ```
22 | ...
23 | "plugins": [
24 | [
25 | "react-native-appsflyer",
26 | {
27 | "shouldUseStrictMode": false, // optional – kids-apps strict mode
28 | "shouldUsePurchaseConnector": true // NEW – enables Purchase Connector
29 | }
30 | ]
31 | ],
32 | ...
33 | ```
34 |
35 | 4. ___optional___ If you are developing a kids app and you wish to use our strict mode, you should add `"shouldUseStrictMode": true` as followed:
36 | ```
37 | ...
38 | "plugins": [
39 | [
40 | "react-native-appsflyer",{"shouldUseStrictMode": true}
41 | ]
42 | ],
43 | ...
44 | ```
45 | ### Fix for build failure with RN 0.76 and Expo 52
46 | To ensure seamless integration of the AppsFlyer plugin in your Expo-managed project, it’s essential to handle modifications to the AndroidManifest.xml correctly. Since direct edits to the AndroidManifest.xml aren’t feasible in the managed workflow, you’ll need to create a custom configuration to include the necessary changes.
47 |
48 | ### Handling dataExtractionRules Conflict
49 |
50 | When building your Expo app with the AppsFlyer plugin, you might encounter a build error related to the `dataExtractionRules` attribute. This issue arises due to a conflict between the `dataExtractionRules `defined in your project’s `AndroidManifest.xml` and the one included in the AppsFlyer SDK.
51 |
52 | Solution: Creating a Custom Plugin to Modify `AndroidManifest.xml`
53 |
54 | To resolve this, you can create a custom Expo config plugin that modifies the AndroidManifest.xml during the build process. This approach allows you to adjust the manifest without directly editing it, maintaining compatibility with the managed workflow.
55 |
56 | Steps to Implement the Custom Plugin:
57 | 1. Create the Plugin File:
58 | - In your project’s root directory, create a file named withCustomAndroidManifest.js.
59 | 2. Define the Plugin Function:
60 | - In withCustomAndroidManifest.js, define a function that uses Expo’s withAndroidManifest to modify the manifest. This function will remove the conflicting dataExtractionRules attribute.
61 |
62 | ```js
63 | // withCustomAndroidManifest.js
64 | const { withAndroidManifest } = require('@expo/config-plugins');
65 |
66 | module.exports = function withCustomAndroidManifest(config) {
67 | return withAndroidManifest(config, async (config) => {
68 | const androidManifest = config.modResults;
69 | const manifest = androidManifest.manifest;
70 |
71 | // Ensure xmlns:tools is present in the tag
72 | if (!manifest.$['xmlns:tools']) {
73 | manifest.$['xmlns:tools'] = 'http://schemas.android.com/tools';
74 | }
75 |
76 | const application = manifest.application[0];
77 |
78 | // Add tools:replace attribute for dataExtractionRules and fullBackupContent
79 | application['$']['tools:replace'] = 'android:dataExtractionRules, android:fullBackupContent';
80 |
81 | // Set dataExtractionRules and fullBackupContent as attributes within
82 | application['$']['android:dataExtractionRules'] = '@xml/secure_store_data_extraction_rules';
83 | application['$']['android:fullBackupContent'] = '@xml/secure_store_backup_rules';
84 |
85 | return config;
86 | });
87 | };
88 |
89 | ```
90 |
91 | 3. Update app.json or app.config.js:
92 | - In your app configuration file, include the custom plugin to ensure it’s executed during the build process.
93 |
94 | ```json
95 | // app.json
96 | {
97 | "expo": {
98 | // ... other configurations ...
99 | "plugins": [
100 | "./withCustomAndroidManifest.js",
101 | [
102 | "react-native-appsflyer",
103 | {
104 | "shouldUseStrictMode": true
105 | }
106 | ]
107 | ]
108 | }
109 | }
110 | ```
111 |
112 | By implementing this custom plugin, you can resolve the dataExtractionRules conflict without directly modifying the AndroidManifest.xml.
113 |
114 | ## The AD_ID permission for android apps
115 | In v6.8.0 of the AppsFlyer SDK, we added the normal permission com.google.android.gms.permission.AD_ID to the SDK's AndroidManifest,
116 | to allow the SDK to collect the Android Advertising ID on apps targeting API 33.
117 | If your app is targeting children, you need to revoke this permission to comply with Google's Data policy.
118 | You can read more about it [here](https://docs.expo.dev/guides/permissions/#android).
119 |
120 | ### Purchase Connector (optional)
121 |
122 | Setting `"shouldUsePurchaseConnector": true` will:
123 |
124 | * **iOS** – add the `PurchaseConnector` CocoaPod automatically
125 | * **Android** – add `appsflyer.enable_purchase_connector=true` to `gradle.properties`
126 |
--------------------------------------------------------------------------------