fix(expo): clean stale tools:replace entries when removing backup attributes#678
Conversation
…ributes 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 AppsFlyerSDK#672
There was a problem hiding this comment.
Pull request overview
This PR updates the Expo Android config plugin to prevent Android manifest merge failures caused by stale tools:replace entries after removing backup-related manifest attributes when preferAppsFlyerBackupRules=true.
Changes:
- Track removed backup-related attributes (
android:dataExtractionRules,android:fullBackupContent) when opting into AppsFlyer backup rules. - Remove any matching attribute names from
tools:replace, deletingtools:replaceentirely if it becomes empty.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
expo/withAppsFlyerAndroid.js
Outdated
| const filtered = appAttrs['tools:replace'] | ||
| .split(',') | ||
| .map((s) => s.trim()) | ||
| .filter((s) => s && !removedKeys.includes(s)); | ||
| if (filtered.length > 0) { |
There was a problem hiding this comment.
The PR description mentions a Set-based deduplication of tools:replace, but this implementation only filters out removedKeys and does not dedupe remaining entries. Either update the description to match the code, or switch this logic to a Set-based normalize+filter so duplicate keys in tools:replace can’t persist and keep breaking manifest merge.
There was a problem hiding this comment.
Good catch on the description. Updated the PR body to say 'filters stale entries' instead of 'Set-based deduplication' — the code only removes the deleted attribute keys from tools:replace and does not otherwise deduplicate remaining entries.
expo/withAppsFlyerAndroid.js
Outdated
| 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'); |
There was a problem hiding this comment.
This log line will print whenever tools:replace exists, even if filtered is identical to the original value (i.e., nothing was actually cleaned). Consider only logging when a change was made, or include the removed keys in the log so it’s not misleading during debugging.
| 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'); | |
| const existingReplaceEntries = appAttrs['tools:replace'] | |
| .split(',') | |
| .map((s) => s.trim()) | |
| .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'); | |
| } |
There was a problem hiding this comment.
Fixed in latest commit — now captures existingReplaceEntries before filtering, and only mutates the attribute and emits the log when filtered.length !== existingReplaceEntries.length (i.e. something was actually removed).
…s 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.
Summary
Fixes a manifest merge failure that occurs when
preferAppsFlyerBackupRules=trueand another Expo plugin (e.g.expo-secure-store) has already added the removed backup attribute names totools:replace.When the plugin deletes
android:dataExtractionRulesorandroid:fullBackupContentfrom the app's manifest, any existingtools:replaceentry referencing those same attribute names becomes stale. The Android manifest merger then fails with:Changes:
removedKeys).tools:replacevalue; deletetools:replaceentirely if it becomes empty.tools:replacecontains no stale entries).Test plan
preferAppsFlyerBackupRules=false(default): no manifest changes, no log outputpreferAppsFlyerBackupRules=true, app has no backup attributes: no manifest changespreferAppsFlyerBackupRules=true, app has backup attributes but notools:replace: attributes removed, notools:replaceclean-up logpreferAppsFlyerBackupRules=true, app has backup attributes +tools:replacecontaining those keys: attributes removed, stale keys filtered fromtools:replace, log emittedpreferAppsFlyerBackupRules=true,tools:replacecontains only stale keys: entiretools:replaceattribute deleted