From b7ad8f53ef4d286f92318ca92e0964b36f26c6a1 Mon Sep 17 00:00:00 2001 From: AmitLY21 Date: Tue, 25 Nov 2025 13:45:24 +0200 Subject: [PATCH 01/15] Add CallbackGuard to prevent double callback invocation on Android --- .../reactnative/RNAppsFlyerModule.java | 164 ++++++++++++------ 1 file changed, 109 insertions(+), 55 deletions(-) diff --git a/android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerModule.java b/android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerModule.java index bb77d936..a8a8b23a 100755 --- a/android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerModule.java +++ b/android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerModule.java @@ -14,6 +14,7 @@ import java.util.Collections; import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; import com.appsflyer.attribution.AppsFlyerRequestListener; import com.appsflyer.deeplink.DeepLinkListener; @@ -47,6 +48,7 @@ import org.json.JSONException; import org.json.JSONObject; +import java.lang.ref.WeakReference; import java.net.URI; import java.util.ArrayList; import java.util.HashMap; @@ -64,6 +66,24 @@ public class RNAppsFlyerModule extends ReactContextBaseJavaModule { private Application application; private String personalDevKey; + private static class CallbackGuard { + private final AtomicBoolean invoked = new AtomicBoolean(false); + private final WeakReference callbackRef; + + public CallbackGuard(Callback callback) { + this.callbackRef = new WeakReference<>(callback); + } + + public void invoke(Object... args) { + if (invoked.compareAndSet(false, true)) { + Callback callback = callbackRef.get(); + if (callback != null) { + callback.invoke(args); + } + } + } + } + public RNAppsFlyerModule(ReactApplicationContext reactContext) { super(reactContext); this.reactContext = reactContext; @@ -107,16 +127,18 @@ public Map getConstants() { @ReactMethod public void initSdkWithCallBack(ReadableMap _options, Callback successCallback, Callback errorCallback) { + CallbackGuard guardedSuccessCallback = new CallbackGuard(successCallback); + CallbackGuard guardedErrorCallback = new CallbackGuard(errorCallback); try { final String errorReason = callSdkInternal(_options); if (errorReason == null) { //TODO: callback should come from SDK - successCallback.invoke(SUCCESS); + guardedSuccessCallback.invoke(SUCCESS); } else { - errorCallback.invoke(new Exception(errorReason).getMessage()); + guardedErrorCallback.invoke(new Exception(errorReason).getMessage()); } } catch (Exception e) { - errorCallback.invoke(e.getMessage()); + guardedErrorCallback.invoke(e.getMessage()); } } @@ -311,9 +333,11 @@ public void logEvent( final String eventName, ReadableMap eventData, final Callback successCallback, final Callback errorCallback) { + final CallbackGuard guardedSuccessCallback = new CallbackGuard(successCallback); + final CallbackGuard guardedErrorCallback = new CallbackGuard(errorCallback); try { if (eventName.trim().equals("")) { - errorCallback.invoke(NO_EVENT_NAME_FOUND); + guardedErrorCallback.invoke(NO_EVENT_NAME_FOUND); return; } Map data = RNUtil.toMap(eventData); @@ -325,17 +349,17 @@ public void logEvent( AppsFlyerLib.getInstance().logEvent(getCurrentActivity(), eventName, data, new AppsFlyerRequestListener() { @Override public void onSuccess() { - successCallback.invoke(SUCCESS); + guardedSuccessCallback.invoke(SUCCESS); } @Override public void onError(int i, @NonNull String s) { - errorCallback.invoke(s); + guardedErrorCallback.invoke(s); } }); } } catch (Exception e) { - errorCallback.invoke(e.getMessage()); + guardedErrorCallback.invoke(e.getMessage()); return; } } @@ -434,48 +458,49 @@ public void logAdRevenue(ReadableMap adRevenueDictionary) { @ReactMethod public void getAppsFlyerUID(Callback callback) { + CallbackGuard guardedCallback = new CallbackGuard(callback); String appId = AppsFlyerLib.getInstance().getAppsFlyerUID(getReactApplicationContext()); - callback.invoke(null, appId); + guardedCallback.invoke(null, appId); } @ReactMethod public void updateServerUninstallToken(final String token, Callback callback) { + CallbackGuard guardedCallback = new CallbackGuard(callback); AppsFlyerLib.getInstance().updateServerUninstallToken(getReactApplicationContext(), token); - if (callback != null) { - callback.invoke(SUCCESS); - } + guardedCallback.invoke(SUCCESS); } @ReactMethod public void setCustomerUserId(final String userId, Callback callback) { + CallbackGuard guardedCallback = new CallbackGuard(callback); AppsFlyerLib.getInstance().setCustomerUserId(userId); - callback.invoke(SUCCESS); + guardedCallback.invoke(SUCCESS); } @ReactMethod public void setCollectIMEI(boolean isCollect, Callback callback) { + CallbackGuard guardedCallback = new CallbackGuard(callback); AppsFlyerLib.getInstance().setCollectIMEI(isCollect); - if (callback != null) { - callback.invoke(SUCCESS); - } + guardedCallback.invoke(SUCCESS); } @ReactMethod public void setCollectAndroidID(boolean isCollect, Callback callback) { + CallbackGuard guardedCallback = new CallbackGuard(callback); AppsFlyerLib.getInstance().setCollectAndroidID(isCollect); - if (callback != null) { - callback.invoke(SUCCESS); - } + guardedCallback.invoke(SUCCESS); } @ReactMethod public void stop(boolean isStopped, Callback callback) { + CallbackGuard guardedCallback = new CallbackGuard(callback); AppsFlyerLib.getInstance().stop(isStopped, getReactApplicationContext()); - callback.invoke(SUCCESS); + guardedCallback.invoke(SUCCESS); } @ReactMethod public void setAdditionalData(ReadableMap additionalData, Callback callback) { + CallbackGuard guardedCallback = new CallbackGuard(callback); Map data = null; try { data = RNUtil.toMap(additionalData); @@ -490,7 +515,7 @@ public void setAdditionalData(ReadableMap additionalData, Callback callback) { HashMap copyData = new HashMap<>(data); AppsFlyerLib.getInstance().setAdditionalData(copyData); - callback.invoke(SUCCESS); + guardedCallback.invoke(SUCCESS); } @@ -498,6 +523,8 @@ public void setAdditionalData(ReadableMap additionalData, Callback callback) { public void setUserEmails(ReadableMap _options, Callback successCallback, Callback errorCallback) { + CallbackGuard guardedSuccessCallback = new CallbackGuard(successCallback); + CallbackGuard guardedErrorCallback = new CallbackGuard(errorCallback); JSONObject options = RNUtil.readableMapToJson(_options); @@ -505,7 +532,7 @@ public void setUserEmails(ReadableMap _options, JSONArray emailsJSON = options.optJSONArray(afEmails); if (emailsJSON.length() == 0) { - errorCallback.invoke(new Exception(EMPTY_OR_CORRUPTED_LIST).getMessage()); + guardedErrorCallback.invoke(new Exception(EMPTY_OR_CORRUPTED_LIST).getMessage()); return; } @@ -525,29 +552,33 @@ public void setUserEmails(ReadableMap _options, } } catch (JSONException e) { e.printStackTrace(); - errorCallback.invoke(new Exception(EMPTY_OR_CORRUPTED_LIST).getMessage()); + guardedErrorCallback.invoke(new Exception(EMPTY_OR_CORRUPTED_LIST).getMessage()); return; } AppsFlyerLib.getInstance().setUserEmails(type, emailsList); - successCallback.invoke(SUCCESS); + guardedSuccessCallback.invoke(SUCCESS); } @ReactMethod public void setAppInviteOneLinkID(final String oneLinkID, Callback callback) { + CallbackGuard guardedCallback = new CallbackGuard(callback); AppsFlyerLib.getInstance().setAppInviteOneLink(oneLinkID); - callback.invoke(SUCCESS); + guardedCallback.invoke(SUCCESS); } @ReactMethod public void setCurrencyCode(final String currencyCode, Callback callback) { + CallbackGuard guardedCallback = new CallbackGuard(callback); AppsFlyerLib.getInstance().setCurrencyCode(currencyCode); - callback.invoke(SUCCESS); + guardedCallback.invoke(SUCCESS); } @ReactMethod public void generateInviteLink(ReadableMap args, final Callback successCallback, final Callback errorCallback) { + final CallbackGuard guardedSuccessCallback = new CallbackGuard(successCallback); + final CallbackGuard guardedErrorCallback = new CallbackGuard(errorCallback); String channel = null; String campaign = null; @@ -614,12 +645,12 @@ public void generateInviteLink(ReadableMap args, final Callback successCallback, CreateOneLinkHttpTask.ResponseListener listener = new CreateOneLinkHttpTask.ResponseListener() { @Override public void onResponse(final String oneLinkUrl) { - successCallback.invoke(oneLinkUrl); + guardedSuccessCallback.invoke(oneLinkUrl); } @Override public void onResponseError(final String error) { - errorCallback.invoke(error); + guardedErrorCallback.invoke(error); } }; @@ -652,14 +683,18 @@ public void logCrossPromotionAndOpenStore(final String appId, final String campa @ReactMethod public void anonymizeUser(boolean b, Callback callback) { + CallbackGuard guardedCallback = new CallbackGuard(callback); AppsFlyerLib.getInstance().anonymizeUser(b); - callback.invoke(SUCCESS); + guardedCallback.invoke(SUCCESS); } @ReactMethod public void setOneLinkCustomDomains(ReadableArray domainsArray, Callback successCallback, Callback errorCallback) { + CallbackGuard guardedSuccessCallback = new CallbackGuard(successCallback); + CallbackGuard guardedErrorCallback = new CallbackGuard(errorCallback); + if (domainsArray.size() <= 0) { - errorCallback.invoke(EMPTY_OR_CORRUPTED_LIST); + guardedErrorCallback.invoke(EMPTY_OR_CORRUPTED_LIST); return; } @@ -668,17 +703,20 @@ public void setOneLinkCustomDomains(ReadableArray domainsArray, Callback success try { String[] domains = domainsList.toArray(new String[domainsList.size()]); AppsFlyerLib.getInstance().setOneLinkCustomDomain(domains); - successCallback.invoke(SUCCESS); + guardedSuccessCallback.invoke(SUCCESS); } catch (Exception e) { e.printStackTrace(); - errorCallback.invoke(EMPTY_OR_CORRUPTED_LIST); + guardedErrorCallback.invoke(EMPTY_OR_CORRUPTED_LIST); } } @ReactMethod public void setResolveDeepLinkURLs(ReadableArray urlsArray, Callback successCallback, Callback errorCallback) { + CallbackGuard guardedSuccessCallback = new CallbackGuard(successCallback); + CallbackGuard guardedErrorCallback = new CallbackGuard(errorCallback); + if (urlsArray.size() <= 0) { - errorCallback.invoke(EMPTY_OR_CORRUPTED_LIST); + guardedErrorCallback.invoke(EMPTY_OR_CORRUPTED_LIST); return; } @@ -687,23 +725,26 @@ public void setResolveDeepLinkURLs(ReadableArray urlsArray, Callback successCall try { String[] urls = urlsList.toArray(new String[urlsList.size()]); AppsFlyerLib.getInstance().setResolveDeepLinkURLs(urls); - successCallback.invoke(SUCCESS); + guardedSuccessCallback.invoke(SUCCESS); } catch (Exception e) { e.printStackTrace(); - errorCallback.invoke(EMPTY_OR_CORRUPTED_LIST); + guardedErrorCallback.invoke(EMPTY_OR_CORRUPTED_LIST); } } @ReactMethod public void performOnAppAttribution(String urlString, Callback successCallback, Callback errorCallback) { + CallbackGuard guardedSuccessCallback = new CallbackGuard(successCallback); + CallbackGuard guardedErrorCallback = new CallbackGuard(errorCallback); + try { URI uri = URI.create(urlString); Context c = application.getApplicationContext(); AppsFlyerLib.getInstance().performOnAppAttribution(c, uri); - successCallback.invoke(SUCCESS); + guardedSuccessCallback.invoke(SUCCESS); } catch (Exception e) { e.printStackTrace(); - errorCallback.invoke(INVALID_URI); + guardedErrorCallback.invoke(INVALID_URI); } } @@ -724,12 +765,16 @@ public void setSharingFilterForPartners(ReadableArray partnersArray) { @ReactMethod public void logLocation(double longitude, double latitude, Callback successCallback) { + CallbackGuard guardedSuccessCallback = new CallbackGuard(successCallback); AppsFlyerLib.getInstance().logLocation(getReactApplicationContext(), latitude, longitude); - successCallback.invoke(SUCCESS); + guardedSuccessCallback.invoke(SUCCESS); } @ReactMethod public void validateAndLogInAppPurchase(ReadableMap purchaseInfo, Callback successCallback, Callback errorCallback) { + CallbackGuard guardedSuccessCallback = new CallbackGuard(successCallback); + CallbackGuard guardedErrorCallback = new CallbackGuard(errorCallback); + String publicKey = ""; String signature = ""; String purchaseData = ""; @@ -753,36 +798,40 @@ public void validateAndLogInAppPurchase(ReadableMap purchaseInfo, Callback succe } if (publicKey == "" || signature == "" || purchaseData == "" || price == "" || currency == "") { - errorCallback.invoke(NO_PARAMETERS_ERROR); + guardedErrorCallback.invoke(NO_PARAMETERS_ERROR); return; } } catch (Exception e) { e.printStackTrace(); - errorCallback.invoke(e); + guardedErrorCallback.invoke(e); return; } - initInAppPurchaseValidatorListener(successCallback, errorCallback); + initInAppPurchaseValidatorListenerInternal(guardedSuccessCallback, guardedErrorCallback); AppsFlyerLib.getInstance().validateAndLogInAppPurchase(reactContext, publicKey, signature, purchaseData, price, currency, additionalParameters); } - @ReactMethod - public void initInAppPurchaseValidatorListener(final Callback successCallback, final Callback errorCallback) { + private void initInAppPurchaseValidatorListenerInternal(final CallbackGuard guardedSuccess, final CallbackGuard guardedError) { AppsFlyerLib.getInstance().registerValidatorListener(reactContext, new AppsFlyerInAppPurchaseValidatorListener() { @Override public void onValidateInApp() { - successCallback.invoke(VALIDATE_SUCCESS); - + guardedSuccess.invoke(VALIDATE_SUCCESS); } @Override public void onValidateInAppFailure(String error) { - errorCallback.invoke(VALIDATE_FAILED + error); - + guardedError.invoke(VALIDATE_FAILED + error); } }); } + @ReactMethod + public void initInAppPurchaseValidatorListener(final Callback successCallback, final Callback errorCallback) { + CallbackGuard guardedSuccess = new CallbackGuard(successCallback); + CallbackGuard guardedError = new CallbackGuard(errorCallback); + initInAppPurchaseValidatorListenerInternal(guardedSuccess, guardedError); + } + @ReactMethod public void validateAndLogInAppPurchaseV2(ReadableMap purchaseDetails, ReadableMap additionalParameters) { try { @@ -859,11 +908,12 @@ private void sendValidationError(String errorMessage) { @ReactMethod public void sendPushNotificationData(ReadableMap pushPayload, Callback errorCallback) { + CallbackGuard guardedErrorCallback = new CallbackGuard(errorCallback); JSONObject payload = RNUtil.readableMapToJson(pushPayload); String errorMsg; if (payload == null) { errorMsg = "PushNotification payload is null"; - handleErrorMessage(errorMsg, errorCallback); + handleErrorMessage(errorMsg, guardedErrorCallback); return; } Bundle bundle = null; @@ -872,7 +922,7 @@ public void sendPushNotificationData(ReadableMap pushPayload, Callback errorCall } catch (JSONException e) { e.printStackTrace(); errorMsg = "Can't parse pushPayload to bundle"; - handleErrorMessage(errorMsg, errorCallback); + handleErrorMessage(errorMsg, guardedErrorCallback); return; } Activity activity = getCurrentActivity(); @@ -884,15 +934,15 @@ public void sendPushNotificationData(ReadableMap pushPayload, Callback errorCall AppsFlyerLib.getInstance().sendPushNotificationData(activity); } else { errorMsg = "The intent is null. Push payload has not been sent!"; - handleErrorMessage(errorMsg, errorCallback); + handleErrorMessage(errorMsg, guardedErrorCallback); } } else { errorMsg = "The activity is null. Push payload has not been sent!"; - handleErrorMessage(errorMsg, errorCallback); + handleErrorMessage(errorMsg, guardedErrorCallback); } } - private void handleErrorMessage(String errorMessage, Callback errorCB) { + private void handleErrorMessage(String errorMessage, CallbackGuard errorCB) { Log.d("AppsFlyer", errorMessage); if (errorCB != null) { errorCB.invoke(errorMessage); @@ -901,14 +951,18 @@ private void handleErrorMessage(String errorMessage, Callback errorCB) { @ReactMethod public void setHost(String hostPrefix, String hostName, Callback successCallback) { + CallbackGuard guardedCallback = new CallbackGuard(successCallback); AppsFlyerLib.getInstance().setHost(hostPrefix, hostName); - successCallback.invoke(SUCCESS); + guardedCallback.invoke(SUCCESS); } @ReactMethod public void addPushNotificationDeepLinkPath(ReadableArray path, Callback successCallback, Callback errorCallback) { + CallbackGuard guardedSuccessCallback = new CallbackGuard(successCallback); + CallbackGuard guardedErrorCallback = new CallbackGuard(errorCallback); + if (path.size() <= 0) { - errorCallback.invoke(EMPTY_OR_CORRUPTED_LIST); + guardedErrorCallback.invoke(EMPTY_OR_CORRUPTED_LIST); return; } // ArrayList pathList = path.toArrayList(); @@ -916,10 +970,10 @@ public void addPushNotificationDeepLinkPath(ReadableArray path, Callback success try { String[] params = pathList.toArray(new String[pathList.size()]); AppsFlyerLib.getInstance().addPushNotificationDeepLinkPath(params); - successCallback.invoke(SUCCESS); + guardedSuccessCallback.invoke(SUCCESS); } catch (Exception e) { e.printStackTrace(); - errorCallback.invoke(e); + guardedErrorCallback.invoke(e); } } From c03c688064b98596ae37ffb5b266fd7859a4616f Mon Sep 17 00:00:00 2001 From: AmitLY21 Date: Tue, 25 Nov 2025 13:49:54 +0200 Subject: [PATCH 02/15] Add ESLint configuration and fix TypeScript definition issues --- .eslintrc.js | 72 +++++++++++++++++++ .../models/canceled_state_context.ts | 6 +- PurchaseConnector/models/test_purchase.ts | 2 +- .../utils/connector_callbacks.ts | 3 +- expo/withAppsFlyerAndroid.js | 5 +- index.d.ts | 41 ++++++----- index.js | 7 +- package.json | 5 ++ 8 files changed, 112 insertions(+), 29 deletions(-) create mode 100644 .eslintrc.js diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..b8f1bfe9 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,72 @@ +/** + * ESLint configuration for react-native-appsflyer plugin + * Optimized for TypeScript + React Native library development + */ + +module.exports = { + root: true, + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module', + ecmaFeatures: { jsx: true }, + }, + + env: { + node: true, + es6: true, + }, + + plugins: [ + '@typescript-eslint', + ], + + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + ], + + rules: { + // JS/TS hygiene + 'no-console': 'off', + '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-var-requires': 'off', // Allow require() in CommonJS files + + // Import/export rules - disabled because TypeScript handles this + // and our index.d.ts properly declares all exports for ESLint compatibility + 'import/default': 'off', + 'import/named': 'off', + 'import/no-unresolved': 'off', + }, + overrides: [ + { + files: ['expo/**/*.js'], + rules: { + '@typescript-eslint/no-var-requires': 'off', // Expo config plugins use require() + }, + }, + { + files: ['**/*.ts', '**/*.tsx'], + rules: { + // TypeScript-specific rules + 'no-undef': 'off', // TypeScript handles this + }, + }, + ], + + ignorePatterns: [ + 'node_modules/**', + 'demos/**', + 'android/**', + 'ios/**', + 'build/**', + 'dist/**', + '__tests__/**', + '*.config.js', + 'babel.config.js', + 'metro.config.js', + 'jest.config.js', + 'react-native.config.js', + ], +}; \ No newline at end of file diff --git a/PurchaseConnector/models/canceled_state_context.ts b/PurchaseConnector/models/canceled_state_context.ts index 17f5f075..c7934e79 100644 --- a/PurchaseConnector/models/canceled_state_context.ts +++ b/PurchaseConnector/models/canceled_state_context.ts @@ -52,7 +52,7 @@ export class CanceledStateContext { class DeveloperInitiatedCancellation { constructor() {} - static fromJson(json: any): DeveloperInitiatedCancellation { + static fromJson(_json: any): DeveloperInitiatedCancellation { // Here you would implement the conversion from JSON to DeveloperInitiatedCancellation instance return new DeveloperInitiatedCancellation(); } @@ -66,7 +66,7 @@ class DeveloperInitiatedCancellation { class ReplacementCancellation { constructor() {} - static fromJson(json: any): ReplacementCancellation { + static fromJson(_json: any): ReplacementCancellation { // Here you would implement the conversion from JSON to ReplacementCancellation instance return new ReplacementCancellation(); } @@ -79,7 +79,7 @@ class ReplacementCancellation { class SystemInitiatedCancellation { constructor() {} - static fromJson(json: any): SystemInitiatedCancellation { + static fromJson(_json: any): SystemInitiatedCancellation { // Here you would implement the conversion from JSON to SystemInitiatedCancellation instance return new SystemInitiatedCancellation(); } diff --git a/PurchaseConnector/models/test_purchase.ts b/PurchaseConnector/models/test_purchase.ts index 0c37093d..773ce788 100644 --- a/PurchaseConnector/models/test_purchase.ts +++ b/PurchaseConnector/models/test_purchase.ts @@ -3,7 +3,7 @@ interface TestPurchaseJson {} export class TestPurchase { constructor() {} - static fromJson(json: TestPurchaseJson): TestPurchase { + static fromJson(_json: TestPurchaseJson): TestPurchase { return new TestPurchase(); } diff --git a/PurchaseConnector/utils/connector_callbacks.ts b/PurchaseConnector/utils/connector_callbacks.ts index 6a0b5a50..da254105 100644 --- a/PurchaseConnector/utils/connector_callbacks.ts +++ b/PurchaseConnector/utils/connector_callbacks.ts @@ -1,4 +1,5 @@ -import { IosError, JVMThrowable } from "../models"; +import { IosError } from "../models/ios_errors"; +import { JVMThrowable } from "../models/jvm_throwable"; // Type definition for a general-purpose listener. export type PurchaseConnectorListener = (data: any) => void; diff --git a/expo/withAppsFlyerAndroid.js b/expo/withAppsFlyerAndroid.js index 24ee78ff..d06bb864 100644 --- a/expo/withAppsFlyerAndroid.js +++ b/expo/withAppsFlyerAndroid.js @@ -51,10 +51,7 @@ function withCustomAndroidManifest(config) { module.exports = function withAppsFlyerAndroid(config, { shouldUsePurchaseConnector = false } = {}) { if (shouldUsePurchaseConnector) { config = addPurchaseConnectorFlag(config); - } else { - console.log('[AppsFlyerPlugin] Purchase Connector disabled, skipping gradle property injection'); - } - + } // Always apply Android manifest modifications for secure data handling config = withCustomAndroidManifest(config); diff --git a/index.d.ts b/index.d.ts index bb0c2260..b0df95ed 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,13 +1,13 @@ /** * Typescript Definition Sync with v5.1.1 **/ -import { InAppPurchaseValidationResult } from "../models/in_app_purchase_validation_result"; -import SubscriptionValidationResult from "../models/subscription_validation_result"; +import InAppPurchaseValidationResult from "./PurchaseConnector/models/in_app_purchase_validation_result"; +import SubscriptionValidationResult from "./PurchaseConnector/models/subscription_validation_result"; import { OnResponse, OnFailure, OnReceivePurchaseRevenueValidationInfo, -} from "../utils/connector_callbacks"; +} from "./PurchaseConnector/utils/connector_callbacks"; declare module "react-native-appsflyer" { type Response = void | Promise; @@ -151,21 +151,17 @@ declare module "react-native-appsflyer" { hasConsentForDataUsage?: boolean, hasConsentForAdsPersonalization?: boolean, hasConsentForAdStorage?: boolean - ) {} + ); /** * @deprecated since version 6.16.2. Use the AppsFlyerConsent constructor instead for more flexibility with optional booleans. */ - static forGDPRUser(hasConsentForDataUsage: boolean, hasConsentForAdsPersonalization: boolean): AppsFlyerConsent { - return new AppsFlyerConsent(true, hasConsentForDataUsage, hasConsentForAdsPersonalization); - } + static forGDPRUser(hasConsentForDataUsage: boolean, hasConsentForAdsPersonalization: boolean): AppsFlyerConsent; /** * @deprecated since version 6.16.2. Use the AppsFlyerConsent constructor instead for more flexibility with optional booleans. */ - static forNonGDPRUser(): AppsFlyerConsent { - return new AppsFlyerConsent(false); - } + static forNonGDPRUser(): AppsFlyerConsent; } /** @@ -201,15 +197,15 @@ declare module "react-native-appsflyer" { mediationNetwork: MEDIATION_NETWORK; currencyIso4217Code: string; revenue: number; - additionalParameters?: StringMap; + additionalParameters?: { [key: string]: any }; } /** * PurchaseConnector */ - export const StoreKitVersion = { - SK1: "SK1", - SK2: "SK2", + export const StoreKitVersion: { + readonly SK1: "SK1"; + readonly SK2: "SK2"; }; export interface PurchaseConnectorConfig { @@ -264,13 +260,13 @@ declare module "react-native-appsflyer" { callback: (data:OnResponse) => any ): () => void; onSubscriptionValidationResultFailure( - callback: (data:onFailure) => any + callback: (data:OnFailure) => any ): () => void; onInAppValidationResultSuccess( callback: (data:OnResponse) => any ): () => void; onInAppValidationResultFailure( - callback: (data:onFailure) => any + callback: (data:OnFailure) => any ): () => void; setSubscriptionPurchaseEventDataSource: (dataSource: SubscriptionPurchaseEventDataSource) => void; @@ -397,7 +393,7 @@ declare module "react-native-appsflyer" { ): void; startSdk(): void; enableTCFDataCollection(enabled: boolean): void; - setConsentData(consentData: AppsFlyerConsentType): void; + setConsentData(consentData: AppsFlyerConsent): void; logAdRevenue(adRevenueData: AFAdRevenueData): void; /** * For iOS Only @@ -420,3 +416,14 @@ declare module "react-native-appsflyer" { export default appsFlyer; } + +// Explicit ambient declarations for ESLint compatibility +// ESLint's import resolver doesn't recognize exports inside 'declare module' blocks. +// These top-level declarations allow ESLint to detect the exports. +declare const StoreKitVersion: { readonly SK1: "SK1"; readonly SK2: "SK2" }; +declare const AppsFlyerPurchaseConnector: any; // Type is defined in declare module above +declare const AppsFlyerPurchaseConnectorConfig: any; // Type is defined in declare module above +declare const appsFlyer: any; // Type is defined in declare module above + +export { StoreKitVersion, AppsFlyerPurchaseConnector, AppsFlyerPurchaseConnectorConfig }; +export { appsFlyer as default }; diff --git a/index.js b/index.js index e19aba6e..f40baf28 100755 --- a/index.js +++ b/index.js @@ -3,6 +3,7 @@ import AppsFlyerConstants from "./PurchaseConnector/constants/constants"; import InAppPurchaseValidationResult from "./PurchaseConnector/models/in_app_purchase_validation_result"; import ValidationFailureData from "./PurchaseConnector/models/validation_failure_data"; import SubscriptionValidationResult from "./PurchaseConnector/models/subscription_validation_result"; +import { MissingConfigurationException } from "./PurchaseConnector/models/missing_configuration_exception"; const { RNAppsFlyer } = NativeModules; const appsFlyer = {}; @@ -186,7 +187,7 @@ AppsFlyerPurchaseConnector.setInAppPurchaseEventDataSource = (dataSource) => { // Purchase Connector iOS methods function logConsumableTransaction(transactionId){ PCAppsFlyer.logConsumableTransaction(transactionId); -}; +} AppsFlyerPurchaseConnector.logConsumableTransaction = logConsumableTransaction; @@ -233,7 +234,7 @@ AppsFlyerPurchaseConnector.setPurchaseRevenueDataSource = (dataSource) => { AppsFlyerPurchaseConnector.setPurchaseRevenueDataSourceStoreKit2 = (dataSource) => { if (!dataSource || typeof dataSource !== 'object') { - throw new Error('dataSource must be an object'); + throw new Error('dataSource must be an object'); } PCAppsFlyer.setPurchaseRevenueDataSourceStoreKit2(dataSource); }; @@ -847,7 +848,7 @@ appsFlyer.setSharingFilterForAllPartners = () => { * @param errorC Error callback */ -appsFlyer.setSharingFilter = (partners, successC, errorC) => { +appsFlyer.setSharingFilter = (partners, _successC, _errorC) => { return appsFlyer.setSharingFilterForPartners(partners); }; diff --git a/package.json b/package.json index cb78cf74..f57fc76f 100755 --- a/package.json +++ b/package.json @@ -6,6 +6,8 @@ "types": "index.d.ts", "scripts": { "test": "jest --coverage", + "lint": "eslint . --ext .js,.jsx,.ts,.tsx", + "lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix", "demo.ios": "npm run ios-pod; cd SampleApps/AppsFlyerExample; npm run ios", "demo.android": "cd SampleApps/AppsFlyerExample; npm run android", "ios-pod": "cd SampleApps/AppsFlyerExample/ios; pod install; cd ../" @@ -32,7 +34,10 @@ "devDependencies": { "@babel/preset-env": "^7.26.9", "@types/jest": "^29.5.14", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", "babel-jest": "^29.7.0", + "eslint": "^8.57.1", "jest": "^29.7.0", "react": "16.11.0", "react-native": "0.62.3", From 392877a5f5591c41c665e0c28cb529b7b88fee61 Mon Sep 17 00:00:00 2001 From: AmitLY21 Date: Tue, 25 Nov 2025 14:44:59 +0200 Subject: [PATCH 03/15] Fix Expo Android build failure by removing secure_store XML (fixes #631) --- expo/withAppsFlyerAndroid.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/expo/withAppsFlyerAndroid.js b/expo/withAppsFlyerAndroid.js index d06bb864..f389c6ca 100644 --- a/expo/withAppsFlyerAndroid.js +++ b/expo/withAppsFlyerAndroid.js @@ -30,18 +30,23 @@ function withCustomAndroidManifest(config) { const application = manifest.application[0]; // Add tools:replace attribute for dataExtractionRules and fullBackupContent + // This allows AppsFlyer SDK's built-in backup rules to take precedence over + // conflicting rules in the app's manifest (see: https://dev.appsflyer.com/hc/docs/install-android-sdk#backup-rules) const existingReplace = application['$']['tools:replace']; if (existingReplace) { - const newReplace = existingReplace + ', android:dataExtractionRules, android:fullBackupContent'; - application['$']['tools:replace'] = newReplace; + // Add to existing tools:replace if not already present + const replaceAttrs = existingReplace.split(',').map(s => s.trim()); + if (!replaceAttrs.includes('android:dataExtractionRules')) { + replaceAttrs.push('android:dataExtractionRules'); + } + if (!replaceAttrs.includes('android:fullBackupContent')) { + replaceAttrs.push('android:fullBackupContent'); + } + application['$']['tools:replace'] = replaceAttrs.join(', '); } else { application['$']['tools:replace'] = 'android:dataExtractionRules, android:fullBackupContent'; } - // Set dataExtractionRules and fullBackupContent as attributes within - application['$']['android:dataExtractionRules'] = '@xml/secure_store_data_extraction_rules'; - application['$']['android:fullBackupContent'] = '@xml/secure_store_backup_rules'; - console.log('[AppsFlyerPlugin] Android manifest modifications completed'); return config; @@ -52,7 +57,8 @@ module.exports = function withAppsFlyerAndroid(config, { shouldUsePurchaseConnec if (shouldUsePurchaseConnector) { config = addPurchaseConnectorFlag(config); } - // Always apply Android manifest modifications for secure data handling + // Apply Android manifest modifications to resolve backup rules conflicts with AppsFlyer SDK + // This ensures AppsFlyer SDK's built-in backup rules take precedence (see issue #631) config = withCustomAndroidManifest(config); return config; From 069c64102d2436434ba7b398149237df13e9c27b Mon Sep 17 00:00:00 2001 From: AmitLY21 Date: Wed, 26 Nov 2025 09:34:18 +0200 Subject: [PATCH 04/15] Add preferAppsFlyerBackupRules flag for Expo Android manifest Add opt-in flag to control backup rules handling. Default respects app's rules; when enabled, removes app's backup rules to use AppsFlyer SDK's built-in rules. --- Docs/RN_ExpoInstallation.md | 47 +++++++++++++++++-- expo/withAppsFlyerAndroid.js | 90 +++++++++++++++++++++++++----------- 2 files changed, 106 insertions(+), 31 deletions(-) diff --git a/Docs/RN_ExpoInstallation.md b/Docs/RN_ExpoInstallation.md index 8be1c6b4..e04aaa75 100644 --- a/Docs/RN_ExpoInstallation.md +++ b/Docs/RN_ExpoInstallation.md @@ -25,7 +25,8 @@ expo install react-native-appsflyer "react-native-appsflyer", { "shouldUseStrictMode": false, // optional – kids-apps strict mode - "shouldUsePurchaseConnector": true // NEW – enables Purchase Connector + "shouldUsePurchaseConnector": true, // optional – enables Purchase Connector + "preferAppsFlyerBackupRules": false // optional – use AppsFlyer SDK backup rules (default: false) } ] ], @@ -42,8 +43,40 @@ expo install react-native-appsflyer ], ... ``` -### Fix for build failure with RN 0.76 and Expo 52 -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. +### Backup Rules Configuration (Android) + +The AppsFlyer SDK includes built-in backup rules in its Android manifest to ensure accurate install/reinstall detection. By default, the plugin respects your app's backup rules and does not modify them. + +**Default Behavior** (`preferAppsFlyerBackupRules: false` or omitted): +- Your app's `android:dataExtractionRules` and `android:fullBackupContent` attributes are left untouched +- You maintain full control over your app's backup policy +- No manifest merge conflicts occur + +**Opt-in Behavior** (`preferAppsFlyerBackupRules: true`): +- If your app defines backup rules, they will be removed to let AppsFlyer SDK's built-in rules take precedence +- This ensures AppsFlyer SDK's backup rules are used, which may improve install/reinstall detection accuracy +- Use this flag if you want AppsFlyer SDK to manage backup rules for you + +**When to use `preferAppsFlyerBackupRules: true`:** +- You want AppsFlyer SDK to handle backup rules automatically +- You're experiencing issues with install/reinstall detection that may be related to backup rules +- You don't have specific backup requirements for your app + +**Example configuration:** +```json +{ + "expo": { + "plugins": [ + [ + "react-native-appsflyer", + { + "preferAppsFlyerBackupRules": true + } + ] + ] + } +} +``` ### Handling dataExtractionRules Conflict @@ -123,3 +156,11 @@ Setting `"shouldUsePurchaseConnector": true` will: * **iOS** – add the `PurchaseConnector` CocoaPod automatically * **Android** – add `appsflyer.enable_purchase_connector=true` to `gradle.properties` + +### Plugin Options Summary + +| Option | Type | Default | Description | +|-------|------|---------|-------------| +| `shouldUseStrictMode` | boolean | `false` | Enable strict mode for kids apps | +| `shouldUsePurchaseConnector` | boolean | `false` | Enable Purchase Connector support | +| `preferAppsFlyerBackupRules` | boolean | `false` | Remove app's backup rules to use AppsFlyer SDK's built-in rules (Android only) | diff --git a/expo/withAppsFlyerAndroid.js b/expo/withAppsFlyerAndroid.js index f389c6ca..bcac54a0 100644 --- a/expo/withAppsFlyerAndroid.js +++ b/expo/withAppsFlyerAndroid.js @@ -13,14 +13,19 @@ function addPurchaseConnectorFlag(config) { }); } -// withCustomAndroidManifest.js -function withCustomAndroidManifest(config) { - return withAndroidManifest(config, async (config) => { +// Opt-in backup rules handling +function withCustomAndroidManifest(config, { preferAppsFlyerBackupRules = false } = {}) { + return withAndroidManifest(config, async (cfg) => { console.log('[AppsFlyerPlugin] Starting Android manifest modifications...'); - - const androidManifest = config.modResults; + + const androidManifest = cfg.modResults; const manifest = androidManifest.manifest; - + + if (!manifest || !manifest.$ || !manifest.application || !manifest.application[0]) { + console.warn('[AppsFlyerPlugin] Unexpected manifest structure; skipping modifications'); + return cfg; + } + // Ensure xmlns:tools is present in the tag if (!manifest.$['xmlns:tools']) { manifest.$['xmlns:tools'] = 'http://schemas.android.com/tools'; @@ -28,38 +33,67 @@ function withCustomAndroidManifest(config) { } const application = manifest.application[0]; + const appAttrs = application.$ || {}; + + const hasDataExtractionRules = appAttrs['android:dataExtractionRules'] !== undefined; + const hasFullBackupContent = appAttrs['android:fullBackupContent'] !== undefined; + + if (!preferAppsFlyerBackupRules) { + // Default: do not touch backup attributes at all + if (hasDataExtractionRules || hasFullBackupContent) { + console.log( + '[AppsFlyerPlugin] App defines backup attributes; leaving them untouched (preferAppsFlyerBackupRules=false)' + ); + } else { + console.log( + '[AppsFlyerPlugin] App does not define backup attributes; no changes required (preferAppsFlyerBackupRules=false)' + ); + } + console.log('[AppsFlyerPlugin] Android manifest modifications completed'); + return cfg; + } - // Add tools:replace attribute for dataExtractionRules and fullBackupContent - // This allows AppsFlyer SDK's built-in backup rules to take precedence over - // conflicting rules in the app's manifest (see: https://dev.appsflyer.com/hc/docs/install-android-sdk#backup-rules) - const existingReplace = application['$']['tools:replace']; - if (existingReplace) { - // Add to existing tools:replace if not already present - const replaceAttrs = existingReplace.split(',').map(s => s.trim()); - if (!replaceAttrs.includes('android:dataExtractionRules')) { - replaceAttrs.push('android:dataExtractionRules'); + // preferAppsFlyerBackupRules === true + if (hasDataExtractionRules || hasFullBackupContent) { + // Remove conflicting attributes from app's manifest + // This allows AppsFlyer SDK's built-in backup rules to be used instead. + if (hasDataExtractionRules) { + delete appAttrs['android:dataExtractionRules']; + console.log( + '[AppsFlyerPlugin] Removed android:dataExtractionRules to use AppsFlyer SDK rules (preferAppsFlyerBackupRules=true)' + ); } - if (!replaceAttrs.includes('android:fullBackupContent')) { - replaceAttrs.push('android:fullBackupContent'); + + if (hasFullBackupContent) { + delete appAttrs['android:fullBackupContent']; + console.log( + '[AppsFlyerPlugin] Removed android:fullBackupContent to use AppsFlyer SDK rules (preferAppsFlyerBackupRules=true)' + ); } - application['$']['tools:replace'] = replaceAttrs.join(', '); } else { - application['$']['tools:replace'] = 'android:dataExtractionRules, android:fullBackupContent'; + console.log( + '[AppsFlyerPlugin] App does not define backup attributes; no conflict with AppsFlyer SDK (preferAppsFlyerBackupRules=true)' + ); } console.log('[AppsFlyerPlugin] Android manifest modifications completed'); - - return config; + return cfg; }); } -module.exports = function withAppsFlyerAndroid(config, { shouldUsePurchaseConnector = false } = {}) { +// Main plugin export +module.exports = function withAppsFlyerAndroid( + config, + { + shouldUsePurchaseConnector = false, + preferAppsFlyerBackupRules = false, + } = {} +) { if (shouldUsePurchaseConnector) { config = addPurchaseConnectorFlag(config); - } - // Apply Android manifest modifications to resolve backup rules conflicts with AppsFlyer SDK - // This ensures AppsFlyer SDK's built-in backup rules take precedence (see issue #631) - config = withCustomAndroidManifest(config); - + } + + config = withCustomAndroidManifest(config, { preferAppsFlyerBackupRules }); + return config; -}; \ No newline at end of file +}; From 47b88a117702111b9b2fb1a35495d7a4cdd8bb22 Mon Sep 17 00:00:00 2001 From: AmitLY21 Date: Wed, 26 Nov 2025 09:48:56 +0200 Subject: [PATCH 05/15] Add BackupRules parameter to main Expo plugin + minimal logs --- expo/withAppsFlyer.js | 8 ++++---- expo/withAppsFlyerAndroid.js | 16 ++-------------- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/expo/withAppsFlyer.js b/expo/withAppsFlyer.js index d7f4956b..decea00a 100644 --- a/expo/withAppsFlyer.js +++ b/expo/withAppsFlyer.js @@ -1,12 +1,12 @@ const withAppsFlyerIos = require('./withAppsFlyerIos'); const withAppsFlyerAndroid = require('./withAppsFlyerAndroid'); -console.log('[AppsFlyerPlugin] Main plugin loaded'); module.exports = function withAppsFlyer(config, { shouldUseStrictMode = false, - shouldUsePurchaseConnector = false + shouldUsePurchaseConnector = false, + preferAppsFlyerBackupRules = false } = {}) { config = withAppsFlyerIos(config, { shouldUseStrictMode, shouldUsePurchaseConnector }); - config = withAppsFlyerAndroid(config, { shouldUsePurchaseConnector }); + config = withAppsFlyerAndroid(config, { shouldUsePurchaseConnector, preferAppsFlyerBackupRules }); return config; -}; +}; \ No newline at end of file diff --git a/expo/withAppsFlyerAndroid.js b/expo/withAppsFlyerAndroid.js index bcac54a0..2debf9f7 100644 --- a/expo/withAppsFlyerAndroid.js +++ b/expo/withAppsFlyerAndroid.js @@ -16,8 +16,6 @@ function addPurchaseConnectorFlag(config) { // Opt-in backup rules handling function withCustomAndroidManifest(config, { preferAppsFlyerBackupRules = false } = {}) { return withAndroidManifest(config, async (cfg) => { - console.log('[AppsFlyerPlugin] Starting Android manifest modifications...'); - const androidManifest = cfg.modResults; const manifest = androidManifest.manifest; @@ -29,7 +27,6 @@ function withCustomAndroidManifest(config, { preferAppsFlyerBackupRules = false // Ensure xmlns:tools is present in the tag if (!manifest.$['xmlns:tools']) { manifest.$['xmlns:tools'] = 'http://schemas.android.com/tools'; - console.log('[AppsFlyerPlugin] Added xmlns:tools namespace'); } const application = manifest.application[0]; @@ -59,24 +56,15 @@ function withCustomAndroidManifest(config, { preferAppsFlyerBackupRules = false // This allows AppsFlyer SDK's built-in backup rules to be used instead. if (hasDataExtractionRules) { delete appAttrs['android:dataExtractionRules']; - console.log( - '[AppsFlyerPlugin] Removed android:dataExtractionRules to use AppsFlyer SDK rules (preferAppsFlyerBackupRules=true)' - ); + console.log('[AppsFlyerPlugin] Removed android:dataExtractionRules to use AppsFlyer SDK rules'); } if (hasFullBackupContent) { delete appAttrs['android:fullBackupContent']; - console.log( - '[AppsFlyerPlugin] Removed android:fullBackupContent to use AppsFlyer SDK rules (preferAppsFlyerBackupRules=true)' - ); + console.log('[AppsFlyerPlugin] Removed android:fullBackupContent to use AppsFlyer SDK rules'); } - } else { - console.log( - '[AppsFlyerPlugin] App does not define backup attributes; no conflict with AppsFlyer SDK (preferAppsFlyerBackupRules=true)' - ); } - console.log('[AppsFlyerPlugin] Android manifest modifications completed'); return cfg; }); } From 5b0eb51df6f763a1126b56f3c024842ca69cc68b Mon Sep 17 00:00:00 2001 From: AmitLY21 Date: Wed, 26 Nov 2025 10:17:29 +0200 Subject: [PATCH 06/15] Enhance Swift header import with fallback for CocoaPods compatibility --- ios/PCAppsFlyer.m | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ios/PCAppsFlyer.m b/ios/PCAppsFlyer.m index 389726bf..9977a729 100644 --- a/ios/PCAppsFlyer.m +++ b/ios/PCAppsFlyer.m @@ -7,7 +7,15 @@ #if __has_include() #import +// Try modular import path first (for newer CocoaPods configurations) +#if __has_include() +#import +#elif __has_include() +// Fallback to legacy import path (for older CocoaPods configurations) #import +#else +#warning "react_native_appsflyer Swift header not found" +#endif @implementation PCAppsFlyer @synthesize bridge = _bridge; From 97884a60db4837bd390700202762ce85b91d178d Mon Sep 17 00:00:00 2001 From: AmitLY21 Date: Thu, 27 Nov 2025 10:07:41 +0200 Subject: [PATCH 07/15] Remove hardcoded swift.h - its auto generated. --- ios/AppsFlyerLib-Swift.h | 331 --------------------------------------- 1 file changed, 331 deletions(-) delete mode 100644 ios/AppsFlyerLib-Swift.h diff --git a/ios/AppsFlyerLib-Swift.h b/ios/AppsFlyerLib-Swift.h deleted file mode 100644 index 091899e5..00000000 --- a/ios/AppsFlyerLib-Swift.h +++ /dev/null @@ -1,331 +0,0 @@ -#if 0 -#elif defined(__arm64__) && __arm64__ -// Generated by Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 clang-1500.1.0.2.5) -#ifndef APPSFLYERLIB_SWIFT_H -#define APPSFLYERLIB_SWIFT_H -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wgcc-compat" - -#if !defined(__has_include) -# define __has_include(x) 0 -#endif -#if !defined(__has_attribute) -# define __has_attribute(x) 0 -#endif -#if !defined(__has_feature) -# define __has_feature(x) 0 -#endif -#if !defined(__has_warning) -# define __has_warning(x) 0 -#endif - -#if __has_include() -# include -#endif - -#pragma clang diagnostic ignored "-Wauto-import" -#if defined(__OBJC__) -#include -#endif -#if defined(__cplusplus) -#include -#include -#include -#include -#include -#include -#include -#else -#include -#include -#include -#include -#endif -#if defined(__cplusplus) -#if defined(__arm64e__) && __has_include() -# include -#else -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wreserved-macro-identifier" -# ifndef __ptrauth_swift_value_witness_function_pointer -# define __ptrauth_swift_value_witness_function_pointer(x) -# endif -# ifndef __ptrauth_swift_class_method_pointer -# define __ptrauth_swift_class_method_pointer(x) -# endif -#pragma clang diagnostic pop -#endif -#endif - -#if !defined(SWIFT_TYPEDEFS) -# define SWIFT_TYPEDEFS 1 -# if __has_include() -# include -# elif !defined(__cplusplus) -typedef uint_least16_t char16_t; -typedef uint_least32_t char32_t; -# endif -typedef float swift_float2 __attribute__((__ext_vector_type__(2))); -typedef float swift_float3 __attribute__((__ext_vector_type__(3))); -typedef float swift_float4 __attribute__((__ext_vector_type__(4))); -typedef double swift_double2 __attribute__((__ext_vector_type__(2))); -typedef double swift_double3 __attribute__((__ext_vector_type__(3))); -typedef double swift_double4 __attribute__((__ext_vector_type__(4))); -typedef int swift_int2 __attribute__((__ext_vector_type__(2))); -typedef int swift_int3 __attribute__((__ext_vector_type__(3))); -typedef int swift_int4 __attribute__((__ext_vector_type__(4))); -typedef unsigned int swift_uint2 __attribute__((__ext_vector_type__(2))); -typedef unsigned int swift_uint3 __attribute__((__ext_vector_type__(3))); -typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); -#endif - -#if !defined(SWIFT_PASTE) -# define SWIFT_PASTE_HELPER(x, y) x##y -# define SWIFT_PASTE(x, y) SWIFT_PASTE_HELPER(x, y) -#endif -#if !defined(SWIFT_METATYPE) -# define SWIFT_METATYPE(X) Class -#endif -#if !defined(SWIFT_CLASS_PROPERTY) -# if __has_feature(objc_class_property) -# define SWIFT_CLASS_PROPERTY(...) __VA_ARGS__ -# else -# define SWIFT_CLASS_PROPERTY(...) -# endif -#endif -#if !defined(SWIFT_RUNTIME_NAME) -# if __has_attribute(objc_runtime_name) -# define SWIFT_RUNTIME_NAME(X) __attribute__((objc_runtime_name(X))) -# else -# define SWIFT_RUNTIME_NAME(X) -# endif -#endif -#if !defined(SWIFT_COMPILE_NAME) -# if __has_attribute(swift_name) -# define SWIFT_COMPILE_NAME(X) __attribute__((swift_name(X))) -# else -# define SWIFT_COMPILE_NAME(X) -# endif -#endif -#if !defined(SWIFT_METHOD_FAMILY) -# if __has_attribute(objc_method_family) -# define SWIFT_METHOD_FAMILY(X) __attribute__((objc_method_family(X))) -# else -# define SWIFT_METHOD_FAMILY(X) -# endif -#endif -#if !defined(SWIFT_NOESCAPE) -# if __has_attribute(noescape) -# define SWIFT_NOESCAPE __attribute__((noescape)) -# else -# define SWIFT_NOESCAPE -# endif -#endif -#if !defined(SWIFT_RELEASES_ARGUMENT) -# if __has_attribute(ns_consumed) -# define SWIFT_RELEASES_ARGUMENT __attribute__((ns_consumed)) -# else -# define SWIFT_RELEASES_ARGUMENT -# endif -#endif -#if !defined(SWIFT_WARN_UNUSED_RESULT) -# if __has_attribute(warn_unused_result) -# define SWIFT_WARN_UNUSED_RESULT __attribute__((warn_unused_result)) -# else -# define SWIFT_WARN_UNUSED_RESULT -# endif -#endif -#if !defined(SWIFT_NORETURN) -# if __has_attribute(noreturn) -# define SWIFT_NORETURN __attribute__((noreturn)) -# else -# define SWIFT_NORETURN -# endif -#endif -#if !defined(SWIFT_CLASS_EXTRA) -# define SWIFT_CLASS_EXTRA -#endif -#if !defined(SWIFT_PROTOCOL_EXTRA) -# define SWIFT_PROTOCOL_EXTRA -#endif -#if !defined(SWIFT_ENUM_EXTRA) -# define SWIFT_ENUM_EXTRA -#endif -#if !defined(SWIFT_CLASS) -# if __has_attribute(objc_subclassing_restricted) -# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_CLASS_EXTRA -# define SWIFT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA -# else -# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA -# define SWIFT_CLASS_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA -# endif -#endif -#if !defined(SWIFT_RESILIENT_CLASS) -# if __has_attribute(objc_class_stub) -# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME) __attribute__((objc_class_stub)) -# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_class_stub)) SWIFT_CLASS_NAMED(SWIFT_NAME) -# else -# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME) -# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) SWIFT_CLASS_NAMED(SWIFT_NAME) -# endif -#endif -#if !defined(SWIFT_PROTOCOL) -# define SWIFT_PROTOCOL(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA -# define SWIFT_PROTOCOL_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA -#endif -#if !defined(SWIFT_EXTENSION) -# define SWIFT_EXTENSION(M) SWIFT_PASTE(M##_Swift_, __LINE__) -#endif -#if !defined(OBJC_DESIGNATED_INITIALIZER) -# if __has_attribute(objc_designated_initializer) -# define OBJC_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer)) -# else -# define OBJC_DESIGNATED_INITIALIZER -# endif -#endif -#if !defined(SWIFT_ENUM_ATTR) -# if __has_attribute(enum_extensibility) -# define SWIFT_ENUM_ATTR(_extensibility) __attribute__((enum_extensibility(_extensibility))) -# else -# define SWIFT_ENUM_ATTR(_extensibility) -# endif -#endif -#if !defined(SWIFT_ENUM) -# define SWIFT_ENUM(_type, _name, _extensibility) enum _name : _type _name; enum SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type -# if __has_feature(generalized_swift_name) -# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) enum _name : _type _name SWIFT_COMPILE_NAME(SWIFT_NAME); enum SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type -# else -# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) SWIFT_ENUM(_type, _name, _extensibility) -# endif -#endif -#if !defined(SWIFT_UNAVAILABLE) -# define SWIFT_UNAVAILABLE __attribute__((unavailable)) -#endif -#if !defined(SWIFT_UNAVAILABLE_MSG) -# define SWIFT_UNAVAILABLE_MSG(msg) __attribute__((unavailable(msg))) -#endif -#if !defined(SWIFT_AVAILABILITY) -# define SWIFT_AVAILABILITY(plat, ...) __attribute__((availability(plat, __VA_ARGS__))) -#endif -#if !defined(SWIFT_WEAK_IMPORT) -# define SWIFT_WEAK_IMPORT __attribute__((weak_import)) -#endif -#if !defined(SWIFT_DEPRECATED) -# define SWIFT_DEPRECATED __attribute__((deprecated)) -#endif -#if !defined(SWIFT_DEPRECATED_MSG) -# define SWIFT_DEPRECATED_MSG(...) __attribute__((deprecated(__VA_ARGS__))) -#endif -#if !defined(SWIFT_DEPRECATED_OBJC) -# if __has_feature(attribute_diagnose_if_objc) -# define SWIFT_DEPRECATED_OBJC(Msg) __attribute__((diagnose_if(1, Msg, "warning"))) -# else -# define SWIFT_DEPRECATED_OBJC(Msg) SWIFT_DEPRECATED_MSG(Msg) -# endif -#endif -#if defined(__OBJC__) -#if !defined(IBSegueAction) -# define IBSegueAction -#endif -#endif -#if !defined(SWIFT_EXTERN) -# if defined(__cplusplus) -# define SWIFT_EXTERN extern "C" -# else -# define SWIFT_EXTERN extern -# endif -#endif -#if !defined(SWIFT_CALL) -# define SWIFT_CALL __attribute__((swiftcall)) -#endif -#if !defined(SWIFT_INDIRECT_RESULT) -# define SWIFT_INDIRECT_RESULT __attribute__((swift_indirect_result)) -#endif -#if !defined(SWIFT_CONTEXT) -# define SWIFT_CONTEXT __attribute__((swift_context)) -#endif -#if !defined(SWIFT_ERROR_RESULT) -# define SWIFT_ERROR_RESULT __attribute__((swift_error_result)) -#endif -#if defined(__cplusplus) -# define SWIFT_NOEXCEPT noexcept -#else -# define SWIFT_NOEXCEPT -#endif -#if !defined(SWIFT_C_INLINE_THUNK) -# if __has_attribute(always_inline) -# if __has_attribute(nodebug) -# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline)) __attribute__((nodebug)) -# else -# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline)) -# endif -# else -# define SWIFT_C_INLINE_THUNK inline -# endif -#endif -#if defined(_WIN32) -#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL) -# define SWIFT_IMPORT_STDLIB_SYMBOL __declspec(dllimport) -#endif -#else -#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL) -# define SWIFT_IMPORT_STDLIB_SYMBOL -#endif -#endif -#if defined(__OBJC__) -#if __has_feature(objc_modules) -#if __has_warning("-Watimport-in-framework-header") -#pragma clang diagnostic ignored "-Watimport-in-framework-header" -#endif -@import Foundation; -@import ObjectiveC; -#endif - -#endif -#pragma clang diagnostic ignored "-Wproperty-attribute-mismatch" -#pragma clang diagnostic ignored "-Wduplicate-method-arg" -#if __has_warning("-Wpragma-clang-attribute") -# pragma clang diagnostic ignored "-Wpragma-clang-attribute" -#endif -#pragma clang diagnostic ignored "-Wunknown-pragmas" -#pragma clang diagnostic ignored "-Wnullability" -#pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" - -#if __has_attribute(external_source_symbol) -# pragma push_macro("any") -# undef any -# pragma clang attribute push(__attribute__((external_source_symbol(language="Swift", defined_in="AppsFlyerLib",generated_declaration))), apply_to=any(function,enum,objc_interface,objc_category,objc_protocol)) -# pragma pop_macro("any") -#endif - -#if defined(__OBJC__) -@class NSNumber; -@class NSCoder; - -SWIFT_CLASS_NAMED("AppsFlyerConsent") -@interface AppsFlyerConsent : NSObject -@property (nonatomic, readonly) BOOL isUserSubjectToGDPR; -@property (nonatomic, readonly) BOOL hasConsentForDataUsage; -@property (nonatomic, readonly) BOOL hasConsentForAdsPersonalization; -@property (nonatomic, readonly, strong) NSNumber * _Nullable hasConsentForAdStorage; -- (nonnull instancetype)init SWIFT_UNAVAILABLE; -+ (nonnull instancetype)new SWIFT_UNAVAILABLE_MSG("-init is unavailable"); -- (nonnull instancetype)initWithNonGDPRUser SWIFT_DEPRECATED_MSG("Use init(isUserSubjectToGDPR:, hasConsentForDataUsage:, hasConsentForAdsPersonalization:, hasConsentForAdStorage:) instead"); -- (nonnull instancetype)initWithIsUserSubjectToGDPR:(NSNumber * _Nullable)isUserSubjectToGDPR hasConsentForDataUsage:(NSNumber * _Nullable)hasConsentForDataUsage hasConsentForAdsPersonalization:(NSNumber * _Nullable)hasConsentForAdsPersonalization hasConsentForAdStorage:(NSNumber * _Nullable)hasConsentForAdStorage; -- (nonnull instancetype)initForGDPRUserWithHasConsentForDataUsage:(BOOL)forGDPRUserWithHasConsentForDataUsage hasConsentForAdsPersonalization:(BOOL)hasConsentForAdsPersonalization SWIFT_DEPRECATED_MSG("Use init(isUserSubjectToGDPR:, hasConsentForDataUsage:, hasConsentForAdsPersonalization:, hasConsentForAdStorage:) instead"); -- (void)encodeWithCoder:(NSCoder * _Nonnull)coder; -- (nullable instancetype)initWithCoder:(NSCoder * _Nonnull)coder; -@end - -#endif -#if __has_attribute(external_source_symbol) -# pragma clang attribute pop -#endif -#if defined(__cplusplus) -#endif -#pragma clang diagnostic pop -#endif - -#else -#error unsupported Swift architecture -#endif From 5ac723e9a83cc621c124a5ff20eb6057338feed1 Mon Sep 17 00:00:00 2001 From: AmitLY21 Date: Thu, 27 Nov 2025 12:37:10 +0200 Subject: [PATCH 08/15] Adding test and update docs --- Docs/RN_API.md | 35 +++++++- Docs/RN_EspIntegration.md | 1 + __tests__/compatibility.test.js | 146 ++++++++++++++++++++++++++++++++ jest.config.js | 2 +- 4 files changed, 179 insertions(+), 5 deletions(-) create mode 100644 __tests__/compatibility.test.js diff --git a/Docs/RN_API.md b/Docs/RN_API.md index 9dcff0cc..06d9daaf 100644 --- a/Docs/RN_API.md +++ b/Docs/RN_API.md @@ -824,12 +824,39 @@ appsFlyer.enableTCFDataCollection(true); ### setConsentData `setConsentData(consentObject): void` -When GDPR applies to the user and your app does not use a CMP compatible with TCF v2.2, use this API to provide the consent data directly to the SDK.
-The AppsFlyerConsent object has 2 methods: +When GDPR applies to the user and your app does not use a CMP compatible with TCF v2.2, use this API to provide the consent data directly to the SDK. -1. `AppsFlyerConsent.forNonGDPRUser`: Indicates that GDPR doesn’t apply to the user and generates nonGDPR consent object. This method doesn’t accept any parameters. -2. `AppsFlyerConsent.forGDPRUser`: create an AppsFlyerConsent object with 2 parameters: +**Recommended approach (since v6.16.2):** +Use the `AppsFlyerConsent` constructor: + +```javascript +import appsFlyer, {AppsFlyerConsent} from 'react-native-appsflyer'; + +// Full consent for GDPR user +const consent1 = new AppsFlyerConsent(true, true, true, true); + +// No consent for GDPR user +const consent2 = new AppsFlyerConsent(true, false, false, false); + +// Non-GDPR user +const consent3 = new AppsFlyerConsent(false); +appsFlyer.setConsentData(consent1); +``` + +**Constructor parameters:** +| parameter | type | description | +| ---------- |----------|------------------ | +| isUserSubjectToGDPR | boolean | Whether GDPR applies to the user (required) | +| hasConsentForDataUsage | boolean | Consent for data usage (optional) | +| hasConsentForAdsPersonalization | boolean | Consent for ads personalization (optional) | +| hasConsentForAdStorage | boolean | Consent for ad storage (optional) | + +**Deprecated approach (still supported):** +The AppsFlyerConsent object has 2 deprecated methods: + +1. `AppsFlyerConsent.forNonGDPRUser`: Indicates that GDPR doesn't apply to the user and generates nonGDPR consent object. This method doesn't accept any parameters. +2. `AppsFlyerConsent.forGDPRUser`: create an AppsFlyerConsent object with 2 parameters: | parameter | type | description | | ---------- |----------|------------------ | diff --git a/Docs/RN_EspIntegration.md b/Docs/RN_EspIntegration.md index eff654c0..a1555a29 100644 --- a/Docs/RN_EspIntegration.md +++ b/Docs/RN_EspIntegration.md @@ -477,6 +477,7 @@ adb shell am start -W -a android.intent.action.VIEW -d "https://your-onelink-dom ``` +See [AppsFlyer Android SDK documentation](https://dev.appsflyer.com/hc/docs/install-android-sdk#backup-rules) for more details. **4. Package attribute deprecated:** - Remove `package="com.yourapp"` from AndroidManifest.xml diff --git a/__tests__/compatibility.test.js b/__tests__/compatibility.test.js new file mode 100644 index 00000000..fb7d917e --- /dev/null +++ b/__tests__/compatibility.test.js @@ -0,0 +1,146 @@ +/** + * Backward Compatibility Tests + * + * These tests verify that changes in this branch don't break existing client code patterns. + * Focus: Runtime compatibility and type safety. + */ + +import appsFlyer, { AppsFlyerConsent, StoreKitVersion } from '../index'; + +describe('Backward Compatibility Tests', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('setConsentData - Runtime Compatibility', () => { + test('setConsentData accepts AppsFlyerConsentType-like plain object at runtime', () => { + // Simulate old code using plain object (AppsFlyerConsentType shape) + const consent = { + isUserSubjectToGDPR: true, + hasConsentForDataUsage: true, + hasConsentForAdsPersonalization: false + }; + + // Should not throw - native code accepts ReadableMap/NSDictionary + expect(() => appsFlyer.setConsentData(consent)).not.toThrow(); + expect(require('../node_modules/react-native/Libraries/BatchedBridge/NativeModules').RNAppsFlyer.setConsentData).toHaveBeenCalled(); + }); + + test('setConsentData accepts AppsFlyerConsent class instance', () => { + // New code using AppsFlyerConsent class + const consent = new AppsFlyerConsent(true, true, false, true); + + expect(() => appsFlyer.setConsentData(consent)).not.toThrow(); + expect(require('../node_modules/react-native/Libraries/BatchedBridge/NativeModules').RNAppsFlyer.setConsentData).toHaveBeenCalled(); + }); + + test('setConsentData accepts minimal consent object (non-GDPR)', () => { + // Minimal object for non-GDPR user + const consent = { + isUserSubjectToGDPR: false + }; + + expect(() => appsFlyer.setConsentData(consent)).not.toThrow(); + }); + + test('setConsentData accepts AppsFlyerConsent with all optional fields', () => { + const consent = new AppsFlyerConsent( + true, // isUserSubjectToGDPR + true, // hasConsentForDataUsage + false, // hasConsentForAdsPersonalization + true // hasConsentForAdStorage + ); + + expect(() => appsFlyer.setConsentData(consent)).not.toThrow(); + }); + }); + + describe('StoreKitVersion - Runtime Access', () => { + test('StoreKitVersion is accessible at runtime as object', () => { + expect(StoreKitVersion).toBeDefined(); + expect(typeof StoreKitVersion).toBe('object'); + expect(StoreKitVersion.SK1).toBe('SK1'); + expect(StoreKitVersion.SK2).toBe('SK2'); + }); + + test('StoreKitVersion can be used in PurchaseConnectorConfig', () => { + const config = { + logSubscriptions: true, + logInApps: true, + sandbox: false, + storeKitVersion: StoreKitVersion.SK1 + }; + + expect(config.storeKitVersion).toBe('SK1'); + expect(config.storeKitVersion).toBe(StoreKitVersion.SK1); + }); + + test('StoreKitVersion values are correct strings', () => { + expect(StoreKitVersion.SK1).toBe('SK1'); + expect(StoreKitVersion.SK2).toBe('SK2'); + expect(typeof StoreKitVersion.SK1).toBe('string'); + expect(typeof StoreKitVersion.SK2).toBe('string'); + }); + }); + + describe('AppsFlyerConsent - Deprecated Static Methods', () => { + test('AppsFlyerConsent.forGDPRUser still works at runtime', () => { + const consent = AppsFlyerConsent.forGDPRUser(true, false); + + expect(consent).toBeInstanceOf(AppsFlyerConsent); + expect(consent.isUserSubjectToGDPR).toBe(true); + expect(consent.hasConsentForDataUsage).toBe(true); + expect(consent.hasConsentForAdsPersonalization).toBe(false); + + // Should work with setConsentData + expect(() => appsFlyer.setConsentData(consent)).not.toThrow(); + }); + + test('AppsFlyerConsent.forNonGDPRUser still works at runtime', () => { + const consent = AppsFlyerConsent.forNonGDPRUser(); + + expect(consent).toBeInstanceOf(AppsFlyerConsent); + expect(consent.isUserSubjectToGDPR).toBe(false); + + // Should work with setConsentData + expect(() => appsFlyer.setConsentData(consent)).not.toThrow(); + }); + }); + + describe('Callback Behavior - Android CallbackGuard (Transparent)', () => { + test('Callbacks still work with initSdk', () => { + const successCallback = jest.fn(); + const errorCallback = jest.fn(); + + const options = { + devKey: 'test', + appId: '123', + isDebug: true + }; + + appsFlyer.initSdk(options, successCallback, errorCallback); + + // CallbackGuard should be transparent - callbacks should still be callable + expect(require('../node_modules/react-native/Libraries/BatchedBridge/NativeModules').RNAppsFlyer.initSdkWithCallBack).toHaveBeenCalled(); + }); + + test('Callbacks still work with logEvent', () => { + const successCallback = jest.fn(); + const errorCallback = jest.fn(); + + appsFlyer.logEvent('test_event', {}, successCallback, errorCallback); + + expect(require('../node_modules/react-native/Libraries/BatchedBridge/NativeModules').RNAppsFlyer.logEvent).toHaveBeenCalled(); + }); + }); + + describe('Type Exports - ESLint Compatibility', () => { + test('All expected exports are available', () => { + expect(appsFlyer).toBeDefined(); + expect(StoreKitVersion).toBeDefined(); + // Note: AppsFlyerPurchaseConnector may not be available if Purchase Connector is disabled + // This test verifies the exports exist, not that they're functional + }); + }); +}); + diff --git a/jest.config.js b/jest.config.js index 0bae7a76..22349596 100644 --- a/jest.config.js +++ b/jest.config.js @@ -15,7 +15,7 @@ module.exports = { modulePathIgnorePatterns: ['/demos/'], testMatch: [ '/__tests__/**/*.test.ts?(x)', - '/__tests__/index.test.js?(x)', + '/__tests__/**/*.test.js?(x)', ], setupFiles: ['/__tests__/setup.js'], }; \ No newline at end of file From 1fb3b483768269adbc2e5a85558c3d5c05fe13fe Mon Sep 17 00:00:00 2001 From: AmitLY21 Date: Thu, 27 Nov 2025 12:37:38 +0200 Subject: [PATCH 09/15] Update changelog --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b666724..56b0f050 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## 6.17.8 +Release date: *TBD* + +- React Native >> Fixed callback double-invocation crash in React Native New Architecture on Android (prevents fatal errors when callbacks are invoked multiple times) +- React Native >> Fixed Expo Android build failure related to missing secure_store XML resources +- React Native >> Added optional `preferAppsFlyerBackupRules` flag for Expo Android manifest backup rules handling +- React Native >> Changed Expo Android default behavior to no longer modify app's backup rules (use `preferAppsFlyerBackupRules: true` to opt-in) +- React Native >> Enhanced iOS Swift header import with fallback for different CocoaPods configurations +- React Native >> Fixed TypeScript definition issues (`onFailure` → `OnFailure` type name, import paths) +- React Native >> Added ESLint configuration and lint scripts for code quality +- React Native >> Update Plugin to v6.17.8 + ## 6.17.5 Release date: *2025-09-04* From 3a5ddd7e1b767c725a60f53cd2dcf3912f52ed67 Mon Sep 17 00:00:00 2001 From: AmitLY21 Date: Sun, 7 Dec 2025 11:54:53 +0200 Subject: [PATCH 10/15] Update SDK to Android 6.17.5/iOS 6.17.8 and deprecate validateAndLog v1 --- Docs/RN_API.md | 38 +++++++++++++------ Docs/RN_InAppEvents.md | 3 ++ android/build.gradle | 2 +- .../reactnative/RNAppsFlyerConstants.java | 2 +- index.d.ts | 7 ++-- index.js | 10 ++--- ios/RNAppsFlyer.h | 2 +- package.json | 2 +- react-native-appsflyer.podspec | 6 +-- 9 files changed, 43 insertions(+), 29 deletions(-) diff --git a/Docs/RN_API.md b/Docs/RN_API.md index 06d9daaf..2c96ff5e 100644 --- a/Docs/RN_API.md +++ b/Docs/RN_API.md @@ -596,6 +596,9 @@ appsFlyer.setSharingFilterForPartners(['googleadwords_int', 'all']); ### validateAndLogInAppPurchase `validateAndLogInAppPurchase(purchaseInfo, successC, errorC): Response` + +> ⚠️ **Deprecated**: This API is deprecated. Use `validateAndLogInAppPurchaseV2` instead. + Receipt validation is a secure mechanism whereby the payment platform (e.g. Apple or Google) validates that an in-app purchase indeed occurred as reported. Learn more - https://support.appsflyer.com/hc/en-us/articles/207032106-Receipt-validation-for-in-app-purchases ❗Important❗ for iOS - set SandBox to ```true``` @@ -628,20 +631,22 @@ appsFlyer.validateAndLogInAppPurchase(info, res => console.log(res), err => cons ``` --- - +**Important Notes:** +- The callback receives both `result` and `error` parameters +- Always check for `error` first before processing `result` +- Handle the case where `AFSDKPurchaseDetails` creation might fail --- ### updateServerUninstallToken diff --git a/Docs/RN_InAppEvents.md b/Docs/RN_InAppEvents.md index 44ec48e4..7c4635d2 100644 --- a/Docs/RN_InAppEvents.md +++ b/Docs/RN_InAppEvents.md @@ -54,6 +54,9 @@ appsFlyer.logEvent( --- ## In-app purchase validation + +> ⚠️ **Deprecated**: The `validateAndLogInAppPurchase` API is deprecated. Use `validateAndLogInAppPurchaseV2` instead. See the [API reference](/Docs/RN_API.md#validateandloginapppurchasev2) for details. + Receipt validation is a secure mechanism whereby the payment platform (e.g. Apple or Google) validates that an in-app purchase indeed occurred as reported. Learn more [here](https://support.appsflyer.com/hc/en-us/articles/207032106-Receipt-validation-for-in-app-purchases). diff --git a/android/build.gradle b/android/build.gradle index 5d236099..4ca9edeb 100755 --- a/android/build.gradle +++ b/android/build.gradle @@ -70,7 +70,7 @@ repositories { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:1.7.10" // Add Kotlin standard library implementation "com.facebook.react:react-native:${safeExtGet('reactNativeVersion', '+')}" - api "com.appsflyer:af-android-sdk:${safeExtGet('appsflyerVersion', '6.17.3')}" + api "com.appsflyer:af-android-sdk:${safeExtGet('appsflyerVersion', '6.17.5')}" implementation "com.android.installreferrer:installreferrer:${safeExtGet('installReferrerVersion', '2.2')}" if (includeConnector){ implementation 'com.appsflyer:purchase-connector:2.1.1' diff --git a/android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerConstants.java b/android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerConstants.java index a566d1b8..11a6fda8 100755 --- a/android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerConstants.java +++ b/android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerConstants.java @@ -6,7 +6,7 @@ public class RNAppsFlyerConstants { - final static String PLUGIN_VERSION = "6.17.7"; + final static String PLUGIN_VERSION = "6.17.8"; final static String NO_DEVKEY_FOUND = "No 'devKey' found or its empty"; final static String UNKNOWN_ERROR = "AF Unknown Error"; final static String SUCCESS = "Success"; diff --git a/index.d.ts b/index.d.ts index b0df95ed..c62d4689 100644 --- a/index.d.ts +++ b/index.d.ts @@ -358,7 +358,7 @@ declare module "react-native-appsflyer" { successC?: SuccessCB ): void; /** - * [LEGACY WARNING] This is the legacy validateAndLogInAppPurchase API. + * @deprecated This API is deprecated. Use validateAndLogInAppPurchaseV2 instead. */ validateAndLogInAppPurchase( purchaseInfo: InAppPurchase, @@ -366,15 +366,14 @@ declare module "react-native-appsflyer" { errorC: ErrorCB ): Response; /** - * [NEW API] This is the new validateAndLogInAppPurchase API with AFPurchaseDetails. + * validateAndLogInAppPurchase API with AFPurchaseDetails. * Uses event emitter pattern for callback handling. - + */ validateAndLogInAppPurchaseV2( purchaseDetails: AFPurchaseDetails, additionalParameters?: { [key: string]: any }, callback?: (data: any) => void ): void; - */ updateServerUninstallToken(token: string, successC?: SuccessCB): void; sendPushNotificationData(pushPayload: object, errorC?: ErrorCB): void; diff --git a/index.js b/index.js index f40baf28..03017e7d 100755 --- a/index.js +++ b/index.js @@ -741,9 +741,8 @@ appsFlyer.onDeepLink = (callback) => { /** - * TODO: Remove comment when the API is stable - * New validateAndLogInAppPurchase API with AFPurchaseDetails support. - * + * validateAndLogInAppPurchase API with AFPurchaseDetails support. + */ appsFlyer.validateAndLogInAppPurchaseV2 = (purchaseDetails, additionalParameters , callback) => { const listener = appsFlyerEventEmitter.addListener("onValidationResult", (_data) => { if (callback && typeof callback === 'function') { @@ -769,7 +768,6 @@ appsFlyer.validateAndLogInAppPurchaseV2 = (purchaseDetails, additionalParameters listener.remove(); }; }; - */ /** * Anonymize user Data. @@ -885,10 +883,10 @@ export const AFPurchaseType = { }; /** - * [LEGACY WARNING] This is the legacy validateAndLogInAppPurchase API. + * @deprecated This API is deprecated. Use validateAndLogInAppPurchaseV2 instead. */ appsFlyer.validateAndLogInAppPurchase = (purchaseInfo, successCallback, errorCallback) => { - console.log('[AppsFlyer] Using legacy validateAndLogInAppPurchase API'); + console.warn('[AppsFlyer] validateAndLogInAppPurchase is deprecated. Use validateAndLogInAppPurchaseV2 instead.'); return RNAppsFlyer.validateAndLogInAppPurchase(purchaseInfo, successCallback, errorCallback); }; diff --git a/ios/RNAppsFlyer.h b/ios/RNAppsFlyer.h index a8780190..09210601 100755 --- a/ios/RNAppsFlyer.h +++ b/ios/RNAppsFlyer.h @@ -22,7 +22,7 @@ @end -static NSString *const kAppsFlyerPluginVersion = @"6.17.7"; +static NSString *const kAppsFlyerPluginVersion = @"6.17.8"; static NSString *const NO_DEVKEY_FOUND = @"No 'devKey' found or its empty"; static NSString *const NO_APPID_FOUND = @"No 'appId' found or its empty"; static NSString *const NO_EVENT_NAME_FOUND = @"No 'eventName' found or its empty"; diff --git a/package.json b/package.json index f57fc76f..bf691d36 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-appsflyer", - "version": "6.17.7", + "version": "6.17.8", "description": "React Native Appsflyer plugin", "main": "index.js", "types": "index.d.ts", diff --git a/react-native-appsflyer.podspec b/react-native-appsflyer.podspec index 7d1c0853..5e35665b 100644 --- a/react-native-appsflyer.podspec +++ b/react-native-appsflyer.podspec @@ -30,19 +30,19 @@ Pod::Spec.new do |s| # AppsFlyerPurchaseConnector if defined?($AppsFlyerPurchaseConnector) && ($AppsFlyerPurchaseConnector == true) Pod::UI.puts "#{s.name}: Including PurchaseConnector." - s.dependency 'PurchaseConnector', '6.17.7' + s.dependency 'PurchaseConnector', '6.17.8' end # AppsFlyerFramework if defined?($RNAppsFlyerStrictMode) && ($RNAppsFlyerStrictMode == true) Pod::UI.puts "#{s.name}: Using AppsFlyerFramework/Strict mode" - s.dependency 'AppsFlyerFramework/Strict', '6.17.7' + s.dependency 'AppsFlyerFramework/Strict', '6.17.8' s.xcconfig = {'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) AFSDK_NO_IDFA=1' } else if !defined?($RNAppsFlyerStrictMode) Pod::UI.puts "#{s.name}: Using default AppsFlyerFramework. You may require App Tracking Transparency. Not allowed for Kids apps." Pod::UI.puts "#{s.name}: You may set variable `$RNAppsFlyerStrictMode=true` in Podfile to use strict mode for kids apps." end - s.dependency 'AppsFlyerFramework', '6.17.7' + s.dependency 'AppsFlyerFramework', '6.17.8' end end From 938e4f59d3346289ade1a403b98ab22f77ebface Mon Sep 17 00:00:00 2001 From: AmitLY21 Date: Sun, 7 Dec 2025 13:20:50 +0200 Subject: [PATCH 11/15] Adding VAL2 testing and linting tests --- __tests__/index.test.js | 57 ++++++++--- __tests__/linting.test.js | 204 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 249 insertions(+), 12 deletions(-) create mode 100644 __tests__/linting.test.js diff --git a/__tests__/index.test.js b/__tests__/index.test.js index 64a0041c..67222b5b 100644 --- a/__tests__/index.test.js +++ b/__tests__/index.test.js @@ -350,7 +350,6 @@ describe("Test appsFlyer API's", () => { appsFlyer.disableAppSetId(); expect(RNAppsFlyer.disableAppSetId).toHaveBeenCalledTimes(1); }); -/* test('it calls appsFlyer.validateAndLogInAppPurchaseV2 with valid purchase details', () => { const purchaseDetails = { purchaseType: 'subscription', @@ -388,7 +387,19 @@ describe("Test appsFlyer API's", () => { appsFlyer.validateAndLogInAppPurchaseV2(purchaseDetails); expect(RNAppsFlyer.validateAndLogInAppPurchaseV2).toHaveBeenCalledTimes(1); }); - */ + + test('it calls appsFlyer.validateAndLogInAppPurchaseV2 with null additional parameters', () => { + const purchaseDetails = { + purchaseType: 'one_time_purchase', + transactionId: 'test_transaction_null', + productId: 'test_product_null' + }; + const callback = jest.fn(); + + appsFlyer.validateAndLogInAppPurchaseV2(purchaseDetails, null, callback); + expect(RNAppsFlyer.validateAndLogInAppPurchaseV2).toHaveBeenCalledTimes(1); + expect(RNAppsFlyer.validateAndLogInAppPurchaseV2).toHaveBeenCalledWith(purchaseDetails, null); + }); test('AFPurchaseType enum values are correct', () => { // Test the enum values directly since they're exported from index.js @@ -610,37 +621,59 @@ describe('Test native event emitter', () => { nativeEventEmitter.emit('onDeepLinking', nativeEventObject); }); -/* test('validateAndLogInAppPurchaseV2 event listener Happy Flow', () => { const validationResult = { result: true, data: { transactionId: 'test_123' } }; let validationListener; + const callback = jest.fn((res) => { + expect(res).toEqual(validationResult); + if (validationListener) validationListener(); + }); validationListener = appsFlyer.validateAndLogInAppPurchaseV2( { purchaseType: 'subscription', transactionId: 'test_123', productId: 'test_product' }, { test: 'param' }, - (res) => { - expect(res).toEqual(validationResult); - validationListener(); - } + callback ); nativeEventEmitter.emit('onValidationResult', JSON.stringify(validationResult)); + expect(callback).toHaveBeenCalledWith(validationResult); }); test('validateAndLogInAppPurchaseV2 event listener with error', () => { const validationError = { error: 'Validation failed' }; let validationListener; + const callback = jest.fn((error) => { + expect(error).toEqual(validationError); + if (validationListener) validationListener(); + }); validationListener = appsFlyer.validateAndLogInAppPurchaseV2( { purchaseType: 'one_time_purchase', transactionId: 'test_456', productId: 'test_product' }, {}, - (error) => { - expect(error).toEqual(validationError); - validationListener(); - } + callback ); nativeEventEmitter.emit('onValidationResult', JSON.stringify(validationError)); + expect(callback).toHaveBeenCalledWith(validationError); + }); + + test('validateAndLogInAppPurchaseV2 event listener with invalid JSON', () => { + const invalidJson = 'not valid json'; + let validationListener; + const callback = jest.fn((error) => { + // AFParseJSONException might not extend Error, check for name property instead + expect(error).toBeDefined(); + expect(error.name).toBe('AFParseJSONException'); + if (validationListener) validationListener(); + }); + + validationListener = appsFlyer.validateAndLogInAppPurchaseV2( + { purchaseType: 'one_time_purchase', transactionId: 'test_789', productId: 'test_product' }, + {}, + callback + ); + + nativeEventEmitter.emit('onValidationResult', invalidJson); + expect(callback).toHaveBeenCalled(); }); - */ }); \ No newline at end of file diff --git a/__tests__/linting.test.js b/__tests__/linting.test.js new file mode 100644 index 00000000..72bd0838 --- /dev/null +++ b/__tests__/linting.test.js @@ -0,0 +1,204 @@ +/** + * Linting Tests + * + * Tests to ensure code quality and linting rules are followed. + * These tests verify that the codebase adheres to ESLint rules. + */ + +const { ESLint } = require('eslint'); +const path = require('path'); +const fs = require('fs'); + +describe('Linting Tests', () => { + let eslint; + + beforeAll(async () => { + try { + eslint = new ESLint({ + useEslintrc: true, + ignore: false, + }); + } catch (error) { + // ESLint might not be available in test environment + console.warn('ESLint not available in test environment, skipping linting tests'); + eslint = null; + } + }); + + describe('JavaScript Files', () => { + test('index.js should pass ESLint', async () => { + if (!eslint) { + // Skip if ESLint is not available + expect(true).toBe(true); + return; + } + + const filePath = path.join(__dirname, '..', 'index.js'); + const results = await eslint.lintFiles([filePath]); + + if (results.length > 0) { + const errors = results[0].messages.filter(m => m.severity === 2); + const warnings = results[0].messages.filter(m => m.severity === 1); + + // Log any issues for debugging + if (errors.length > 0 || warnings.length > 0) { + // Use console.info instead of console.log to avoid test warnings + // eslint-disable-next-line no-console + console.info('ESLint issues found:', results[0].messages); + } + + expect(errors).toHaveLength(0); + } else { + expect(true).toBe(true); + } + }); + + test('Expo config plugins should pass ESLint', async () => { + if (!eslint) { + expect(true).toBe(true); + return; + } + + const expoDir = path.join(__dirname, '..', 'expo'); + const files = fs.readdirSync(expoDir) + .filter(f => f.endsWith('.js')) + .map(f => path.join(expoDir, f)); + + if (files.length > 0) { + const results = await eslint.lintFiles(files); + + results.forEach((result, index) => { + const errors = result.messages.filter(m => m.severity === 2); + if (errors.length > 0) { + // eslint-disable-next-line no-console + console.info(`ESLint errors in ${files[index]}:`, errors); + } + expect(errors).toHaveLength(0); + }); + } + }); + }); + + describe('TypeScript Files', () => { + test('Purchase Connector models should pass ESLint', async () => { + const purchaseConnectorDir = path.join(__dirname, '..', 'PurchaseConnector'); + + const getAllTsFiles = (dir) => { + let results = []; + const list = fs.readdirSync(dir); + list.forEach(file => { + const filePath = path.join(dir, file); + const stat = fs.statSync(filePath); + if (stat && stat.isDirectory()) { + results = results.concat(getAllTsFiles(filePath)); + } else if (file.endsWith('.ts') || file.endsWith('.tsx')) { + results.push(filePath); + } + }); + return results; + }; + + const tsFiles = getAllTsFiles(purchaseConnectorDir); + + if (!eslint) { + expect(true).toBe(true); + return; + } + + if (tsFiles.length > 0) { + const results = await eslint.lintFiles(tsFiles); + + results.forEach((result, index) => { + const errors = result.messages.filter(m => m.severity === 2); + if (errors.length > 0) { + // eslint-disable-next-line no-console + console.info(`ESLint errors in ${tsFiles[index]}:`, errors); + } + expect(errors).toHaveLength(0); + }); + } + }); + }); + + describe('Code Quality Rules', () => { + test('No console.log statements in production code', async () => { + const indexJsPath = path.join(__dirname, '..', 'index.js'); + const content = fs.readFileSync(indexJsPath, 'utf8'); + + // Allow console.warn and console.error, but check for console.log + const consoleLogMatches = content.match(/console\.log\(/g); + + // console.log is allowed in this codebase (see .eslintrc.js: no-console: 'off') + // But we can still check for excessive usage + // Note: This is informational only, not a failure + const logCount = consoleLogMatches ? consoleLogMatches.length : 0; + + // This test passes but we track console.log usage + expect(logCount).toBeGreaterThanOrEqual(0); + }); + + test('No unused variables in test files', async () => { + const testFiles = [ + path.join(__dirname, 'index.test.js'), + path.join(__dirname, 'compatibility.test.js'), + ].filter(f => fs.existsSync(f)); + + if (testFiles.length > 0) { + const results = await eslint.lintFiles(testFiles); + + results.forEach((result, index) => { + const unusedVarErrors = result.messages.filter( + m => m.ruleId === '@typescript-eslint/no-unused-vars' && m.severity === 2 + ); + + if (unusedVarErrors.length > 0) { + // eslint-disable-next-line no-console + console.info(`Unused variables in ${testFiles[index]}:`, unusedVarErrors); + } + + // Allow unused vars with _ prefix (see .eslintrc.js) + const nonPrefixedUnused = unusedVarErrors.filter( + e => !e.message.includes('_') + ); + expect(nonPrefixedUnused).toHaveLength(0); + }); + } + }); + }); + + describe('Import/Export Consistency', () => { + test('index.js exports should match index.d.ts declarations', () => { + const indexJsPath = path.join(__dirname, '..', 'index.js'); + const indexDtsPath = path.join(__dirname, '..', 'index.d.ts'); + + const jsContent = fs.readFileSync(indexJsPath, 'utf8'); + const dtsContent = fs.readFileSync(indexDtsPath, 'utf8'); + + // Check for key exports + const keyExports = [ + 'AFPurchaseType', + 'AppsFlyerConsent', + 'StoreKitVersion', + 'MEDIATION_NETWORK' + ]; + + keyExports.forEach(exportName => { + // Check JS export + const jsExported = jsContent.includes(`export const ${exportName}`) || + jsContent.includes(`export { ${exportName}`) || + jsContent.includes(`exports.${exportName}`); + + // Check TypeScript declaration + const dtsDeclared = dtsContent.includes(exportName); + + if (jsExported && !dtsDeclared) { + console.warn(`${exportName} is exported in JS but not declared in TypeScript`); + } + + // This is informational - not a hard failure + expect(true).toBe(true); + }); + }); + }); +}); + From a774d7303f15a09e15541b256120c48dec0245ff Mon Sep 17 00:00:00 2001 From: AmitLY21 Date: Wed, 17 Dec 2025 10:35:22 +0200 Subject: [PATCH 12/15] Added try/catch on CallbackGuard and updated changelog --- CHANGELOG.md | 28 +++++++++++++------ .../reactnative/RNAppsFlyerModule.java | 15 ++++++++-- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56b0f050..d801e703 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,25 @@ ## 6.17.8 -Release date: *TBD* - -- React Native >> Fixed callback double-invocation crash in React Native New Architecture on Android (prevents fatal errors when callbacks are invoked multiple times) -- React Native >> Fixed Expo Android build failure related to missing secure_store XML resources -- React Native >> Added optional `preferAppsFlyerBackupRules` flag for Expo Android manifest backup rules handling -- React Native >> Changed Expo Android default behavior to no longer modify app's backup rules (use `preferAppsFlyerBackupRules: true` to opt-in) -- React Native >> Enhanced iOS Swift header import with fallback for different CocoaPods configurations -- React Native >> Fixed TypeScript definition issues (`onFailure` → `OnFailure` type name, import paths) -- React Native >> Added ESLint configuration and lint scripts for code quality +Release date: *2025-12-17* + +- React Native >> Update Android SDK to 6.17.5 +- React Native >> Update iOS SDK to 6.17.8 +- React Native >> Update iOS Purchase Connector to 6.17.8 +- React Native >> Fixed callback double-invocation crash in React Native New Architecture on Android +- React Native >> Fixed TypeScript return type for `validateAndLogInAppPurchaseV2` +- React Native >> Fixed TypeScript type issues (`onFailure` → `OnFailure`, import paths) +- React Native >> Fixed Expo Android build failure related to backup rules +- React Native >> Added `preferAppsFlyerBackupRules` flag for Expo Android (default: false) +- React Native >> Enhanced iOS Swift header import with CocoaPods fallback +- React Native >> Added ESLint configuration and lint scripts - React Native >> Update Plugin to v6.17.8 +## 6.17.7 +Release date: 2025-10-22 + +- React Native >> Update Android SDK to 6.17.7 +- React Native >> Update iOS SDK to 6.17.7 +- React Native >> Update Plugin to v6.17.7 + ## 6.17.5 Release date: *2025-09-04* diff --git a/android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerModule.java b/android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerModule.java index a8a8b23a..66674b10 100755 --- a/android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerModule.java +++ b/android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerModule.java @@ -67,6 +67,7 @@ public class RNAppsFlyerModule extends ReactContextBaseJavaModule { private String personalDevKey; private static class CallbackGuard { + private static final String TAG = "AppsFlyer_" + RNAppsFlyerConstants.PLUGIN_VERSION; private final AtomicBoolean invoked = new AtomicBoolean(false); private final WeakReference callbackRef; @@ -76,9 +77,19 @@ public CallbackGuard(Callback callback) { public void invoke(Object... args) { if (invoked.compareAndSet(false, true)) { - Callback callback = callbackRef.get(); + // Store in local strong reference to prevent race condition with GC + final Callback callback = callbackRef.get(); if (callback != null) { - callback.invoke(args); + try { + callback.invoke(args); + } catch (RuntimeException | IllegalStateException e) { + // Log error when bridge is destroyed or context is dead + // Don't rethrow - callback failure shouldn't break the SDK + Log.e(TAG, "Failed to invoke callback - bridge may be destroyed", e); + } catch (Exception e) { + // Catch any other unexpected exceptions + Log.e(TAG, "Unexpected error invoking callback", e); + } } } } From 3acb1656434d2c3898d67087e5ede29ac588d0af Mon Sep 17 00:00:00 2001 From: AmitLY21 Date: Wed, 17 Dec 2025 10:47:09 +0200 Subject: [PATCH 13/15] fix: RuntimeException already catches IllegalStateException --- .../main/java/com/appsflyer/reactnative/RNAppsFlyerModule.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerModule.java b/android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerModule.java index 66674b10..a095ccfb 100755 --- a/android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerModule.java +++ b/android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerModule.java @@ -82,7 +82,7 @@ public void invoke(Object... args) { if (callback != null) { try { callback.invoke(args); - } catch (RuntimeException | IllegalStateException e) { + } catch (RuntimeException e) { // Log error when bridge is destroyed or context is dead // Don't rethrow - callback failure shouldn't break the SDK Log.e(TAG, "Failed to invoke callback - bridge may be destroyed", e); From ffb403b5e2ab2b2c174231815d761c30697c1fe9 Mon Sep 17 00:00:00 2001 From: "Amit.kremer" Date: Wed, 17 Dec 2025 09:06:01 +0000 Subject: [PATCH 14/15] 6.17.8-rc1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bf691d36..1185ac67 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-appsflyer", - "version": "6.17.8", + "version": "6.17.8-rc1", "description": "React Native Appsflyer plugin", "main": "index.js", "types": "index.d.ts", From 89e5f37162ec62e025363d86d1eb6e79d04fb461 Mon Sep 17 00:00:00 2001 From: AmitLY21 Date: Wed, 17 Dec 2025 11:55:41 +0200 Subject: [PATCH 15/15] bump version to 6.17.8 for production release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1185ac67..bf691d36 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-appsflyer", - "version": "6.17.8-rc1", + "version": "6.17.8", "description": "React Native Appsflyer plugin", "main": "index.js", "types": "index.d.ts",