Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1f34628
feat: migrate core JS SDK to @appsflyer-sdk/js-core-plugin
al-af Aug 12, 2026
e25ce49
refactor(android): convert RNAppsFlyerConstants and RNUtil from Java …
al-af Aug 12, 2026
68304ff
fix(android): catch unexpected exceptions in executeRpc
al-af Aug 12, 2026
fc808e9
chore: bump Kotlin toolchain to 2.4.10 across demo/example apps
al-af Aug 12, 2026
788b870
refactor(purchase-connector): simplify Android/iOS purchase connector…
al-af Aug 12, 2026
a89a699
feat(ios): add AppsFlyerAttribution bridgeReady gate for AppDelegate-…
al-af Aug 12, 2026
6ac4423
fix(demo): migrate AppsFlyer.js to js-core-plugin object-param API
al-af Aug 12, 2026
36a5eae
feat(demo): add invite/share button and debug result modal
al-af Aug 12, 2026
1408010
refactor(example): rewrite QA test app for js-core-plugin API and ses…
al-af Aug 12, 2026
658160b
fix(demo): fix expo app session bootstrap and correct RPC catalog for…
al-af Aug 12, 2026
7d67f4a
chore(demo): pin transitive brace-expansion override versions in expo…
al-af Aug 12, 2026
a2ed420
chore: fix af-scenario-runner.sh simulator log-container resolution
al-af Aug 12, 2026
f2d4cfa
docs: update migration guide and product docs for js-core migration
al-af Aug 12, 2026
9ec7e20
chore(android): bump AGP to 9.2.1 and force patched transitive CVE deps
al-af Aug 12, 2026
e6b91c5
fix(example/android): pin kotlin-gradle-plugin to kotlinVersion
al-af Aug 12, 2026
f4b172b
perf(android): split RPC dispatch into a listener-lifecycle lane and …
al-af Aug 12, 2026
8e63ebb
chore(demo): add uuid dependency override to expo app
al-af Aug 12, 2026
c771824
test: assert js-core-plugin's fabricated NOT_FOUND status instead of …
al-af Aug 12, 2026
28c6268
docs: fix stale initSdk references and broken PurchaseConnector start…
al-af Aug 12, 2026
34899fc
fix(deep-link): enforce platform-specific listener order for deep links
al-af Aug 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/commands/release-check.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Verify all release checkpoints. Report as a pass/fail checklist.
- `package.json` version
- `react-native-appsflyer.podspec` s.version
- `ios/RNAppsFlyer.h` kAppsFlyerPluginVersion
- `android/.../RNAppsFlyerConstants.java` PLUGIN_VERSION
- `android/.../RNAppsFlyerConstants.kt` PLUGIN_VERSION

2. **CHANGELOG** — `CHANGELOG.md` has an entry for the current version at the top.

Expand Down
2 changes: 1 addition & 1 deletion .claude/commands/version-bump.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Bump the plugin version to `$ARGUMENTS` across all 4 files that must stay in syn
1. `package.json` — `"version": "X.Y.Z"`
2. `react-native-appsflyer.podspec` — `s.version = 'X.Y.Z'`
3. `ios/RNAppsFlyer.h` — `kAppsFlyerPluginVersion = @"X.Y.Z"`
4. `android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerConstants.java` — `PLUGIN_VERSION = "X.Y.Z"`
4. `android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerConstants.kt` — `PLUGIN_VERSION = "X.Y.Z"`

### Steps

Expand Down
72 changes: 52 additions & 20 deletions .claude/rules/bridge-patterns.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
---
paths:
- "index.js"
- "index.d.ts"
- "index.ts"
- "src/NativeAppsFlyer.ts"
- "src/rn-transport.ts"
---

# Bridge patterns — JS ↔ native contract

Scope: `index.js`, `index.d.ts`, `src/NativeAppsFlyer.ts`. All native calls go through the single TurboModule entry point `NativeAppsFlyer.executeRpc(requestJson)` — there are no bespoke per-feature native methods.
Scope: `index.ts`, `src/NativeAppsFlyer.ts`, `src/rn-transport.ts`. All native calls go through the single TurboModule entry point `NativeAppsFlyer.executeRpc(requestJson)` — there are no bespoke per-feature native methods.

## 1. Three call patterns (all route through executeRpc)
Since the js-core migration, method dispatch (`callRpc`/`callRpcVoid`-style logic), per-platform wire method-name/param resolution, and event demuxing all live inside the `@appsflyer-sdk/js-core-plugin` npm package, not in this repo. `callRpc`/`callRpcVoid`/`callRpcWithCallback` no longer exist here. This repo's only remaining framework-specific glue is `src/rn-transport.ts`'s `RNTransport`, which implements `@appsflyer-sdk/js-core-plugin`'s `RpcTransport` interface:

| Pattern | Helper | When to use |
|---------|--------|-------------|
| Promise-returning | `callRpc(method, params)` | Any method that returns data or needs error handling |
| Void config setter | `callRpcVoid(method, params)` | Fire-and-forget setters; logs a warning on failure instead of throwing |
| Callback compat | `callRpcWithCallback(method, params, successCb)` | Legacy callback-style API surface; bridges to `callRpc` internally |
| `RpcTransport` member | Implementation |
|---|---|
| `call<T>(method, params)` | Serializes to `executeRpc`'s request JSON, parses the response, resolves with `data` or rejects with `error` |
| `subscribe(listener)` | Wraps `NativeEventEmitter` on the shared `RNAppsFlyer_rpcEvent` event name |

When adding a new method, pick the pattern that matches the method's JS contract. Do not add a fourth pattern.
`index.ts` constructs `AppsFlyerSDK` with an `RNTransport` instance and re-exports it (`export const AppsFlyer = sdk`) plus everything from `@appsflyer-sdk/js-core-plugin` (`export * from "@appsflyer-sdk/js-core-plugin"`). It only adds two platform-specific overrides on top (mediation-network wire-value resolution for `logAdRevenue`, and a string-splicing fix for `setUserFbLoginId`'s big-integer precision) — see the comments above each override in `index.ts` for why they can't live in the platform-agnostic shared package.

## 2. RPC request/response shape

Expand All @@ -33,19 +32,17 @@ Every response resolves (never rejects for native-side outcomes) as:
{ "success": false, "error": { "code": <number>, "message": "<string>" } }
```

`callRpc` unwraps this: resolves with `data` on success, rejects with `error` on failure.
`RNTransport.call` (the `RpcTransport.call` implementation) unwraps this: resolves with `data` on success, rejects with `error` on failure. `@appsflyer-sdk/js-core-plugin`'s `AppsFlyerSDK` methods call `RNTransport.call` internally — this repo no longer calls it directly except from `index.ts`'s two per-platform overrides.

**Android cross-platform note**: Android maps unknown-method to error code 422 with message `"Unknown or missing method: ..."`. `callRpc` normalizes this to `{ code: 404 }` to match iOS's dedicated 404 — see `contracts/rpc-error-normalization-contract.md`.
**Android cross-platform note**: Android maps unknown-method to error code 422 with message `"Unknown or missing method: ..."`, normalized to `{ code: 404 }` to match iOS's dedicated 404 — see `specs/001-turbomodule-rpc-bridge/contracts/rpc-error-normalization-contract.md`. (Not observed in `RNTransport` or the current `@appsflyer-sdk/js-core-plugin` dist — verify this still holds if debugging a 422/404 mismatch.)

The TurboModule Promise rejects (transport failure) only if the call never reaches native at all.

## 3. Event channel contract

Async native events (conversion data, deep link, session ready) arrive via `NativeEventEmitter` on a **single shared event name** (`RNAppsFlyer_rpcEvent` on both platforms).

`index.js` demuxes on `envelope.event` — one of:
Async native events (conversion data, deep link, session ready) arrive via `NativeEventEmitter` on a **single shared event name** (`RNAppsFlyer_rpcEvent` on both platforms). `RNTransport.subscribe` forwards the raw envelope to `@appsflyer-sdk/js-core-plugin`, which now owns the demuxing (this repo no longer parses `envelope.event` itself):
- `onConversionDataSuccess` / `onConversionDataFail`
- `onDeepLinkReceived` (iOS) / `onDeepLinking` (Android) — same concept, different native name; `index.js` normalizes both
- `onDeepLinkReceived` (iOS) / `onDeepLinking` (Android) — same concept, different native name; normalized to one JS-facing shape
- `onSessionReady` — both platforms emit this once `registerSessionReadyListener` has been registered and the native SDK signals readiness (confirmed against `AppsFlyerRPC`'s own source, `AFRPCCoreHandler.swift`'s `sessionReadyEmitter`). `isSessionReady` is a separate one-off Promise query for the current state, not a replacement for the event.

The raw `origin` and `timestamp` envelope fields are stripped before handing `data` to app callbacks. There is no `supportedEvents` array to maintain under TurboModules.
Expand All @@ -60,6 +57,40 @@ native SDK singleton, with no state check on `init`. The iOS `AppsFlyerRPC` READ
this explicitly as intended parity with the native SDK — only `start`/`logEvent` require `init`
to have run first; listener registration does not.

**Two confirmed exceptions to that claim, both inside the vendored `AppsFlyerLib` binary
underneath `AppsFlyerRPC` (not fixable from this repo), where the delegate *assignment itself*
is harmless but triggers a side effect that isn't init-order-safe** — see `known-issues-kb.md`
for full root-cause detail on each:
- `registerSessionReadyListener` — `AppsFlyerLib.m`'s `registerSessionReadyListener:` asserts
`devKey`/`appleAppID` are already set, and racing it against `init()`'s own unstructured Task
can crash the app outright. Must be called only after `init()` has resolved.
- `registerDeepLinkListener` (**iOS only** — see below for Android) — `AppsFlyerLib.m`'s
`setDeepLinkDelegate:` fires a **one-shot** (`dispatch_once`) deferred-deep-link resolution
request immediately on assignment, using whatever host config exists at that moment. Calling
it before `init()` has configured the host burns that one-shot attempt on a malformed URL,
permanently (for the rest of that app process's lifetime — not retried). Must also be called
only after `init()` has resolved.

`registerConversionListener` has no such exception (`setDelegate:` only assigns the ivar and
logs a deprecation warning) and may still register before `init()` per the general rule above.

**`registerDeepLinkListener` is platform-split — the two native SDKs are misaligned on when
it's safe to attach the listener, so this is the one call whose position moves relative to
`init()` by platform:**
- **iOS**: register *after* `init()` — the one-shot DDL bug above.
- **Android**: register *before* `init()`. `AFDeepLinkManager`'s `onDeepLinking()` /
`onDeepLinkingSuccess()` / `onDeepLinkingError()` guard on `if (listener != null)` with zero
buffering — a result delivered before the listener is attached is dropped permanently. In the
typical single-Activity RN launch this was previously masked by an incidental lifecycle-timing
gap (see `known-issues-kb.md`'s Android deep-link entry for the full analysis) that made
"register after `init()`" appear safe — but that's a timing accident, not a guarantee, and it
doesn't hold for apps with a trampoline/splash launcher Activity. Register before `init()` on
Android instead of relying on it. `index.ts`/samples do this via `Platform.OS === 'android'`.

This is the only listener where call order differs by platform — `registerConversionListener`
and `registerSessionReadyListener` both keep the single "synchronously, right after `init()`"
rule on both platforms.

There used to be a JS-repo-side buffer (`RpcInitGate.kt` on Android, an equivalent
`initCompleted`/`pendingRegistrations` gate in `RNAppsFlyerImpl.swift`) that held these RPCs
until `init` resolved, on the assumption native silently dropped early registrations. That
Expand All @@ -81,7 +112,8 @@ delays the *dispatch*, and delayed dispatch of `registerSessionReadyListener` de
callback that's supposed to trigger `start()` (see the recommended pattern below).
`example/src/App.tsx` calls `init()` first and registers listeners as separate synchronous
statements right after it, matching the reference `RPCTestApp`'s own call order (`initialize` →
`isDebug` → listeners → ... → `start`).
`isDebug` → listeners → ... → `start`) — except `registerDeepLinkListener`, which it calls
before `init()` on Android per the platform split above, via `Platform.OS`.

### Recommended pattern for deterministic ordering after start()

Expand Down Expand Up @@ -119,13 +151,13 @@ session-ready-stall entry for the one confirmed native cause).

## 5. No transpilation

`index.js` ships as-is via npm — no Babel, no bundler. Write only syntax that Metro and Node can consume directly.
`index.ts` ships as-is via npm (no separate `index.js`/`index.d.ts` pair) — no Babel, no bundler. Write only syntax that Metro and Node can consume directly.

## 6. Named exports

Current named exports from `index.js`: `AppsFlyerConsent`, `AFInAppEventType`, `AFPurchaseType`, `MEDIATION_NETWORK`, `StoreKitVersion`, `AppsFlyerPurchaseConnector`, `AppsFlyerPurchaseConnectorConfig`.
Current named exports from `index.ts`: `AFInAppEventType`, `AFPurchaseType`, `MEDIATION_NETWORK`, `StoreKitVersion`, `AppsFlyerPurchaseConnector`, `AppsFlyerPurchaseConnectorConfig`, plus everything `@appsflyer-sdk/js-core-plugin` exports (via `export * from "@appsflyer-sdk/js-core-plugin"`) — including `AppsFlyerConsent`, which now lives in that package, not this repo.

`AFInAppEventType` is now a plain JS frozen object (23 constants) — it was previously served by `NativeModules.RNAppsFlyer.getConstants()`. Adding a new named export requires a version bump and matching `index.d.ts` update.
`AFInAppEventType` is a plain JS frozen object (23 constants) — it was previously served by `NativeModules.RNAppsFlyer.getConstants()`. Adding a new named export requires a version bump.

## 7. PurchaseConnector

Expand Down
Loading
Loading