Skip to content

JS Core Migration - #699

Merged
al-af merged 20 commits into
developmentfrom
dev/js-core-migration
Aug 13, 2026
Merged

JS Core Migration#699
al-af merged 20 commits into
developmentfrom
dev/js-core-migration

Conversation

@al-af

@al-af al-af commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Migrate core JS SDK to @appsflyer-sdk/js-core-plugin

Summary

Replaces this repo's hand-maintained RPC dispatch, method-name resolution, and event-demuxing logic with the shared @appsflyer-sdk/js-core-plugin npm package. index.ts now constructs AppsFlyerSDK from that package with a thin RNTransport implementing its RpcTransport interface, instead of owning callRpc/callRpcVoid/callRpcWithCallback logic directly. Platform-specific glue (mediation-network wire-value resolution, setUserFbLoginId big-integer precision) stays in index.ts as documented overrides.

Why

Centralizes RPC dispatch/wire-contract logic in one shared package instead of duplicating it per-platform-SDK-wrapper, so wire-contract fixes and new RPC methods land once upstream instead of being re-implemented here.

What changed

Core migration

  • index.ts rebuilt on @appsflyer-sdk/js-core-plugin's AppsFlyerSDK + RNTransport (src/rn-transport.ts) — see .claude/rules/bridge-patterns.md for the updated contract.
  • Android: converted RNAppsFlyerConstants/RNUtil Java → Kotlin; executeRpc now catches unexpected exceptions instead of letting them crash the bridge; RPC dispatch split into a dedicated listener-lifecycle lane and a pooled lane for throughput.
  • iOS: RNAppsFlyerImpl.swift updated for the new transport contract; added AppsFlyerAttribution bridge-ready gate so AppDelegate-level deep links buffer correctly until start() has actually fired (previously they could be delivered to a not-yet-configured SDK).
  • Purchase Connector: simplified Android/iOS wrappers, dropped a redundant wrapper class — untouched otherwise (out of scope for the RPC rewrite per project convention).

Deep-link listener registration order (native SDK misalignment)

  • registerDeepLinkListener's required position relative to init() is now platform-split:
    • Android: register before init() — the native SDK has no buffering for a deep link delivered before a listener is attached; it drops permanently. (Previously "after init()" appeared safe only due to incidental RN launch-order timing — not a real guarantee, and it doesn't hold for trampoline/splash launcher Activities.)
    • iOS: register after init() — the native SDK fires a one-shot deferred-deep-link resolution the instant the listener is attached; doing so before init() has configured the host burns that attempt permanently for the process's lifetime.
  • Updated Docs/RN_API.md, Docs/RN_UnifiedDeepLink.md, Docs/RN_Integration.md with the Platform.OS-conditional pattern and rationale.
  • Updated example/src/App.tsx, demos/appsflyer-expo-app/App.js, demos/appsflyer-react-native-app/components/AppsFlyer.js to match.

CI

  • android-e2e.yml, ios-e2e.yml, and lint-test-build.yml's build-android/build-ios jobs now run npm install at the repo root before example's install — Metro couldn't resolve @appsflyer-sdk/js-core-plugin from the root-level index.ts (reached via example's file:.. dependency) because the new dependency was never installed anywhere CI could find it. Root cause of the two failing E2E runs this branch was built to fix.

Demo apps / tooling

  • Expo app: rewrote RPC catalog and session bootstrap for the new API, added invite/share button and debug result modal.
  • Example (QA) app: rewritten for the js-core-plugin API with a registerSessionReadyListener timeout fallback (native listener is known to stall on some launches — see known-issues KB).
  • Misc: Kotlin toolchain bump to 2.4.10, AGP bump to 9.2.1 with patched transitive CVE deps, pinned kotlin-gradle-plugin/uuid/brace-expansion versions in demo apps, af-scenario-runner.sh fix for stale simulator log-container resolution.

Docs

  • MIGRATION.md and product docs updated for the js-core migration; fixed stale initSdk references and a broken PurchaseConnector start() snippet.

Breaking changes

None beyond what's already tracked for 7.0.0 in MIGRATION.md/CHANGELOG.md — this branch is an internal implementation swap (dispatch/wire logic → shared package), not a new public API surface change. Consuming apps' registerDeepLinkListener call sites should adopt the new platform-conditional order (see Docs/RN_UnifiedDeepLink.md).

Testing

  • npm test (Jest, with coverage) and npm run lint clean.
  • npx tsc --noEmit clean on the touched files (one pre-existing unrelated error in src/rn-transport.ts, not introduced by this branch).
  • Android/iOS E2E workflows (android-e2e.yml, ios-e2e.yml) — root cause of prior failures fixed; expect green on next run given the missing root npm install step is now added.
  • Manual smoke pattern per MIGRATION.md's checklist: install → conversion/deep-link callback → session-ready → start() → logEvent().

al-af and others added 20 commits August 12, 2026 10:06
index.ts now delegates method dispatch, per-platform wire resolution, and event demuxing to the shared js-core-plugin package via a new RNTransport (src/rn-transport.ts). callRpc/callRpcVoid/callRpcWithCallback no longer exist in this repo. PurchaseConnector/index.ts adds a stable barrel export so PurchaseConnector's internal layout can change without touching index.ts. Test files updated to match the new module-load-time RNTransport construction and normalized error/response shapes.
…to Kotlin

Pure language conversion, no behavior change. Updates every doc/command/CI reference to the .java path to point at .kt instead (release-check, version-bump, release-versioning.md, native-android.md, promote-release.yml, release.yml).
executeRpc runs on rpcExecutor's background thread; an uncaught exception there terminates the whole process via Android's default uncaught-exception handler, and the JS promise never resolves either way. AppsFlyerRpcHandler is a vendored dependency we do not control, so wrap dispatchToNative and normalize any unexpected Exception into the standard error response instead of letting it escape.
example and demos/appsflyer-react-native-app bump kotlinVersion from 2.1.20 to 2.4.10. android/build.gradle (this plugin's own module) drops its independently-pinned kotlin-gradle-plugin classpath entry and kotlin-stdlib 1.7.10 pin in favor of inheriting the host app's Kotlin compiler plus a kotlin_stdlib_version default of 2.4.10 to match -- a second, independently-versioned Kotlin plugin here previously caused a compiled-with-incompatible-Kotlin-version failure (version skew, not a stale pin).

NOTE: android/build.gradle also contains an unrelated hunk (adding src/main/common to sourceSets) that belongs with the purchase-connector reorg commit -- folded in here rather than run as a separate git add -p pass. Split with git add -p android/build.gradle if you want that hunk on the other commit.
…, drop redundant wrapper class

Android: PCAppsFlyerPackage.java moves from excludeConnector to a shared common/ dir instead of being duplicated in includeConnector too (the includeConnector copy and MappedValidationResultListener.java -- a one-caller wrapper around the native PurchaseClient.ValidationResultListener -- are deleted; callers use the native interface directly). iOS: AFTransactionFetcher.swift and PCAppsFlyer.h/.m drop dead code, stale comments, an unused import, and an errorAsDictionary helper duplicated at its only two call sites.
…level deep link buffering

New AppsFlyerAttribution.swift buffers continueUserActivity/handleOpen(url:) calls made from the host app's AppDelegate (cold-start Universal Link / custom-scheme open) until RNAppsFlyerImpl's start RPC has succeeded, avoiding the unconfigured-host failure mode and the nobody-listening drop documented in known-issues-kb.md. RNAppsFlyerImpl.executeRpc flips bridgeReady on a successful start response. App-side AppDelegates (example, demos/appsflyer-react-native-app, and the Expo config plugin's injected template) now route through AppsFlyerAttribution.shared instead of calling AppsFlyerLib.shared() directly for these two calls.
AppsFlyer.js: every call site now passes a single params object per the new SDK surface (init, enableDebug, registerConversionListener, registerDeepLinkListener, setCurrentDeviceLanguage). AFInit no longer returns unsubscribe functions (the new SDK does not provide unregister-by-reference); replaced with an AFCleanup export wired into HomeScreen's effect cleanup. Also fixes an Android cold-start deep link bug: the native SDK does not inspect the launch Intent until init() has actually completed, so getInitialURL's re-delivery via performDeepLinking now always runs after init resolves instead of only inside an Android-only branch that ran before init could finish. registerSessionReadyListener registration now bails early if init failed, avoiding an assert-crash. HomeScreen.js also updates its deep-link status check from the stale lowercase 'found' to the real 'FOUND' enum value.
Cart.js adds a Share and invite friends button using generateInviteLink + the Share API. New ResultModal.js displays the raw JSON payload of the last conversion-data/deep-link callback for on-device debugging; wired into HomeScreen.js in the previous commit.
…sion-ready timeout fallback

Every call site updated to the single-params-object API. Adds startWhenSessionReady(), which wraps registerSessionReadyListener + start() in a Promise with a timeout fallback so a stalled native session-ready callback cannot hang the whole auto-run flow.
… js-core-plugin API

App.js's bootstrap effect never actually called init() -- a dead, unreachable arrow function above it was supposed to and never ran, so registerSessionReadyListener fired against whatever native state was left from a previous run. Replaced with one bootstrap() that awaits init(), registers listeners only after it resolves, and starts via a startWhenSessionReady() helper with a timeout fallback.

rpcCatalog.js was calling the pre-migration positional-arg API (setCustomerUserId('x'), logEvent('name', {}, true)) and importing the no-longer-exported AppsFlyerConsent class. Rewritten against the actual object-param signatures, including two platform-specific corrections the .d.ts does not encode: sendPushNotificationData is Android-only (ios: null in the real RPC map) and validateAndLogInAppPurchase's purchase field is a platform oneOf (iOS: transactionId, Android: purchaseToken).
… app

Replaces a single brace-expansion override with per-major-version pins (1.x, 2.x, 5.x) so all resolved majors get the patched version instead of just one.
Resolves the QA log file via simctl get_app_container instead of find-ing across every container on disk -- orphaned containers from past runs could cause find | head -1 to return a stale container's log instead of the current install's.
Reflects index.ts's new single-file entry point (no separate index.js/index.d.ts), the AppsFlyerConsent class removal in favor of a plain setConsentData object, RNAppsFlyerConstants.kt path, and updated native SDK version numbers (Android 7.0.1, iOS AppsFlyerRPC 7.0.12, RN >=0.76 New Architecture requirement).
AGP 9.2.1 still pulls vulnerable bouncycastle/jdom2/jose4j transitives; force pinned patched versions in the buildscript classpath.
classpath had no version, relying on implicit resolution.
…a pooled lane

The 6 register/unregister listener calls touch AppsFlyerRpcHandler's unsynchronized fields and need strict FIFO ordering; everything else is a stateless passthrough. Single-thread executor previously serialized all RPCs, so a slow start/logEvent call head-of-line-blocked fast ones behind it. Adds isListenerLifecycleCall + unit test covering the routing table.
…raw pass-through

registerDeepLinkListener normalizes every payload on the merged onDeepLinkReceived/onDeepLinking channel and defaults a missing status field to NOT_FOUND (dist/appsflyer-sdk.js normalizeDeepLinkStatus), including legacy attribution-only payloads that never had a status. Not fixable from this repo -- js-core-plugin is a compiled dependency and index.ts has no interception point before normalization runs. Updated the three affected tests to assert the dependency's actual behavior and documented the root cause in known-issues-kb.md.
…() snippet

RN_DeepLinkIntegrate.md and RN_EspIntegration.md still referenced the removed initSdk name. RN_PurchaseConnector.md's core-init example called start() standalone instead of inside registerSessionReadyListener, violating the start() contract -- copy-pasting it would silently drop attribution. RN_ExpoDeepLinkIntegration.md and RN_ExpoInstallation.md were missing the RN >= 0.76 / New Architecture prerequisite that is mandatory as of 7.0.0.
The `registerDeepLinkListener` call order relative to `init()` is now platform-split due to native SDK constraints:
- **iOS**: Must be registered *after* `init()`. Calling it early triggers a one-shot deferred deep link (DDL) request against an unconfigured host, permanently breaking DDL for the app process.
- **Android**: Must be registered *before* `init()`. The native SDK has no buffering for early DDL results; an unattached listener drops events permanently. Previously, incidental RN lifecycle timing masked this, but it was unreliable.

All documentation (`RN_API.md`, `RN_Integration.md`, `RN_UnifiedDeepLink.md`, `bridge-patterns.md`, `known-issues-kb.md`) and sample applications have been updated with `Platform.OS` conditionals to reflect this critical behavior.

Also includes minor CI workflow fixes to install plugin dependencies and comment cleanups in native modules.
@al-af
al-af requested a review from pazlavi August 13, 2026 11:47
@al-af al-af self-assigned this Aug 13, 2026
@al-af
al-af merged commit 786deae into development Aug 13, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants