From 9d91188b6e3d5ac63465732c60b58ccf66fc85f8 Mon Sep 17 00:00:00 2001 From: Matin Gathani Date: Fri, 10 Apr 2026 16:29:17 -0700 Subject: [PATCH 1/2] fix(expo): clean stale tools:replace entries when removing backup attributes When preferAppsFlyerBackupRules=true, the plugin removes android:dataExtractionRules and android:fullBackupContent from the app manifest. If another Expo plugin (e.g. expo-secure-store) previously added those attribute names to tools:replace, they become orphaned after deletion and cause an Android manifest merge failure: "Multiple entries with same key: android:dataExtractionRules=REPLACE" After removing each attribute, filter it out of any existing tools:replace value using a Set-based dedup, removing the entry entirely if no other keys remain. Fixes #672 --- expo/withAppsFlyerAndroid.js | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/expo/withAppsFlyerAndroid.js b/expo/withAppsFlyerAndroid.js index 2debf9f7..964bafbe 100644 --- a/expo/withAppsFlyerAndroid.js +++ b/expo/withAppsFlyerAndroid.js @@ -52,17 +52,38 @@ function withCustomAndroidManifest(config, { preferAppsFlyerBackupRules = false // preferAppsFlyerBackupRules === true if (hasDataExtractionRules || hasFullBackupContent) { - // Remove conflicting attributes from app's manifest + // Remove conflicting attributes from app's manifest. // This allows AppsFlyer SDK's built-in backup rules to be used instead. + const removedKeys = []; + if (hasDataExtractionRules) { delete appAttrs['android:dataExtractionRules']; + removedKeys.push('android:dataExtractionRules'); console.log('[AppsFlyerPlugin] Removed android:dataExtractionRules to use AppsFlyer SDK rules'); } if (hasFullBackupContent) { delete appAttrs['android:fullBackupContent']; + removedKeys.push('android:fullBackupContent'); console.log('[AppsFlyerPlugin] Removed android:fullBackupContent to use AppsFlyer SDK rules'); } + + // Clean removed attribute names from any existing tools:replace to avoid stale replace + // directives. Other plugins (e.g. expo-secure-store) may have already added these keys + // to tools:replace; leaving them after the attributes are deleted causes a manifest merge + // failure: "Multiple entries with same key: =REPLACE". + if (appAttrs['tools:replace']) { + const filtered = appAttrs['tools:replace'] + .split(',') + .map((s) => s.trim()) + .filter((s) => s && !removedKeys.includes(s)); + if (filtered.length > 0) { + appAttrs['tools:replace'] = filtered.join(', '); + } else { + delete appAttrs['tools:replace']; + } + console.log('[AppsFlyerPlugin] Cleaned stale tools:replace entries for removed backup attributes'); + } } return cfg; From 4ca5bb6d1c02cd383e4040a1d1e0051bfd05cf7f Mon Sep 17 00:00:00 2001 From: Matin Gathani Date: Sat, 11 Apr 2026 14:26:14 -0700 Subject: [PATCH 2/2] Address Copilot review: only log and mutate tools:replace when entries were removed Previously the log line '[AppsFlyerPlugin] Cleaned stale tools:replace entries' printed unconditionally whenever tools:replace existed, even when no backup attribute keys were present in it (i.e. nothing was actually cleaned). Now: capture the original entry list, compute the filtered list, and only update the attribute and emit the log when the two lists differ in length. --- expo/withAppsFlyerAndroid.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/expo/withAppsFlyerAndroid.js b/expo/withAppsFlyerAndroid.js index 964bafbe..b73146c8 100644 --- a/expo/withAppsFlyerAndroid.js +++ b/expo/withAppsFlyerAndroid.js @@ -73,16 +73,20 @@ function withCustomAndroidManifest(config, { preferAppsFlyerBackupRules = false // to tools:replace; leaving them after the attributes are deleted causes a manifest merge // failure: "Multiple entries with same key: =REPLACE". if (appAttrs['tools:replace']) { - const filtered = appAttrs['tools:replace'] + const existingReplaceEntries = appAttrs['tools:replace'] .split(',') .map((s) => s.trim()) - .filter((s) => s && !removedKeys.includes(s)); - if (filtered.length > 0) { - appAttrs['tools:replace'] = filtered.join(', '); - } else { - delete appAttrs['tools:replace']; + .filter(Boolean); + const filtered = existingReplaceEntries.filter((s) => !removedKeys.includes(s)); + + if (filtered.length !== existingReplaceEntries.length) { + if (filtered.length > 0) { + appAttrs['tools:replace'] = filtered.join(', '); + } else { + delete appAttrs['tools:replace']; + } + console.log('[AppsFlyerPlugin] Cleaned stale tools:replace entries for removed backup attributes'); } - console.log('[AppsFlyerPlugin] Cleaned stale tools:replace entries for removed backup attributes'); } }