Fix iOS callback ownership across Flutter engines#456
Open
TeddyYeung wants to merge 1 commit into
Open
Conversation
37537c2 to
33c8645
Compare
33c8645 to
df45d8f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes iOS callback delivery so registering
appsflyer_sdkon a secondary FlutterEngine does not rebind durable AppsFlyer callbacks away from the engine that owns the active listener.Background
Flutter apps can register plugins more than once in the same iOS process when they create an additional FlutterEngine, for example a background engine that reuses
GeneratedPluginRegistrant.Before this change, the iOS plugin kept Flutter callback channels in static/global channel state. That meant a later plugin registration could overwrite the callback channel used by callbacks such as conversion data, app-open attribution, and UDL/deep-linking. In that state, a callback could be invoked on the most recently registered engine instead of the engine that called
startListening.This PR keeps the native AppsFlyer SDK state process-global, but separates Flutter callback delivery by callback id:
Changes
startListening.callbackChannelgetter remains for legacy native callers.Compatibility
Validation
Native regression coverage:
AppsflyerSdkPluginMultiEngineTestscreates separate fakeFlutterBinaryMessengerobjects to model a main engine and a secondary engine in the same process.testDeepLinkCallbackOwnerSurvivesSecondaryEngineRegistrationverifies that registering a secondary engine does not move an existing deep-link callback owner.testSecondaryEngineCannotStealExistingDeepLinkListenerverifies that a secondary engine callingstartListeningfor an already-owned durable callback id does not steal delivery.testSecondaryEngineCancelDoesNotRemoveDeepLinkListenerOwnerverifies that non-owner cancellation does not clear the real owner or callback flag.testCallbackOwnerCanMoveAfterOwnerCancelsListeningverifies ownership transfer after explicit owner cancellation.testCallbackOwnerCanMoveAfterOwnerIsDeallocatedverifies ownership cleanup when the owning plugin instance is deallocated without Dart-side cancellation.testInstallConversionCallbackUsesOwningEngineverifies the same durable ownership behavior for install conversion callbacks.testRequestScopedCallbackUsesLatestEngineverifies request-scoped callbacks can move to the latest engine.Commands run:
git diff --check: passed.dart analyze --no-fatal-warnings: exit 0 with existing warnings/info.flutter test: passed.xcodebuild test -workspace Runner.xcworkspace -scheme Runner -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.4' -quiet: passed, including allAppsflyerSdkPluginMultiEngineTests.flutter build ios --simulatorfromexample: passed.Local iOS validation used a temporary ignored
example/.envfile for the example app asset requirement. The file is not part of this PR.Notes
This addresses the multi-engine registration class of issues where apps reuse
GeneratedPluginRegistrantfor a background engine. It does not change AppsFlyer's native SDK singleton behavior or introduce any Dart-side API changes.