Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
129 commits
Select commit Hold shift + click to select a range
9b8c59b
Merge branch 'master' into development
al-af May 19, 2026
2544fe0
Prevent final release on workflow cancellation
al-af May 19, 2026
6c945aa
chore: add react-doctor dev tooling and config
al-af Jun 2, 2026
8ff8fed
fix(demo): force a single react-native copy in metro for local linking
al-af Jun 2, 2026
b3512dc
fix(demo): load AppsFlyer devKey/appId from env and fire ad revenue once
al-af Jun 2, 2026
54e0599
feat(demo): add app launcher icon for iOS and Android
al-af Jun 2, 2026
f15be5e
feat(demo): redesign product list, cards, and cart with quantity grou…
al-af Jun 2, 2026
3fd85a6
feat(demo): redesign product detail page
al-af Jun 2, 2026
2e00a6d
feat(demo): redesign welcome modal
al-af Jun 2, 2026
b9e19db
Merge branch 'dev/example-app-update' into development
al-af Jun 3, 2026
af32599
chore: add speckit skill scaffolding
al-af Jul 23, 2026
92e96ae
chore: update gitignore, test plan, and scenario runner script
al-af Jul 23, 2026
f7d6444
docs: update rules, CLAUDE.md, and changelog for TurboModule RPC rewrite
al-af Jul 23, 2026
a99b122
feat(ios): replace per-method bridge with executeRpc TurboModule
al-af Jul 23, 2026
b0e5871
feat(android): replace per-method bridge with executeRpc TurboModule
al-af Jul 23, 2026
0cf18da
feat: add executeRpc TurboModule spec and typed JS API surface
al-af Jul 23, 2026
8ab4406
test: cover executeRpc contract, threading, and updated JS API
al-af Jul 23, 2026
e37eacc
feat(demo): migrate example app to executeRpc TurboModule bridge
al-af Jul 23, 2026
f65cb81
feat(demo): migrate appsflyer-react-native-app to Swift AppDelegate a…
al-af Jul 23, 2026
0870c57
feat(demo): update expo app and config plugin for TurboModule bridge
al-af Jul 23, 2026
5ea8020
fix(bridge): remove listener-registration buffer and session-ready fa…
al-af Aug 5, 2026
06798e6
fix(demo): harden regex handling and update dependency for robustness
al-af Aug 6, 2026
fffa71a
chore(deps): remove react-doctor
al-af Aug 6, 2026
08d945d
fix(demo): harden regex handling and update dependency for robustness
al-af Aug 6, 2026
36340aa
chore(deps): scope fast-uri override to Expo demo app
al-af Aug 6, 2026
fe448f5
fix: correct RPC wire-contract mismatches in JS API and iOS bridge
al-af Jul 28, 2026
4ff4dfb
test: add RPC wire-contract generator scripts and contract test
al-af Jul 28, 2026
4d6549b
test: update index and compatibility tests for RPC API fixes
al-af Jul 28, 2026
a60145c
chore: migrate Android/iOS native deps to Maven/CocoaPods (Phase B)
al-af Jul 28, 2026
86227db
fix(demo): align sample apps with RPC API fixes and clean up demo bugs
al-af Jul 28, 2026
e0aaa67
chore: add rn-architect-guardrails skill for React Native demo review
al-af Jul 28, 2026
5c980e2
docs: update API docs, migration guide, and changelog for RPC fixes
al-af Jul 28, 2026
6009a85
fix(android): release rpcExecutor on TurboModule invalidate
al-af Jul 28, 2026
2d00fbf
fix(example): unregister session-ready listener before calling startSdk
al-af Jul 29, 2026
70669e5
refactor(demo): register session-ready listener after init() call for…
al-af Jul 29, 2026
36b95a2
fix: normalize startSdk errors, drop isSessionReady side effect, pres…
al-af Jul 29, 2026
19319b6
fix(ios): correct stale RNAppsFlyer.m project reference to .mm
al-af Jul 29, 2026
0243bda
fix(ios): fail podspec build script when umbrella header patch target…
al-af Jul 29, 2026
66bbb0e
fix: source iOS RPC contract fixture pod version from the podspec pin
al-af Jul 29, 2026
15f38c4
docs: correct shared RPC event name in bridge rules
al-af Jul 29, 2026
91a6bdb
docs: remove stale pre-7.0.0 API references across Docs/
al-af Jul 29, 2026
5221219
chore: replace sed-based native dependency version bumps with scripts…
al-af Jul 29, 2026
3d7dcbf
chore: misc tooling housekeeping
al-af Jul 29, 2026
5f50818
docs: rewrite MIGRATION.md
al-af Jul 30, 2026
09f8514
docs: remove stale AppsFlyerAttribution references from deep-link docs
al-af Jul 30, 2026
288e89e
fix(android): use af-android-sdk-bom, downgrade af-android-sdk to imp…
al-af Aug 5, 2026
ccb78ed
refactor: consolidate index.js/index.d.ts into a typed index.ts entry…
al-af Aug 2, 2026
e67cf64
test: update test suite for index.ts entry point and start() rename
al-af Aug 2, 2026
3230c72
docs: document start() rename and Promise-only API
al-af Aug 2, 2026
bf2f55e
chore(qa): update test-plan fixtures for start() rename
al-af Aug 2, 2026
f87ebac
chore(demo): update expo rpcCatalog for start() rename and dropped iO…
al-af Aug 2, 2026
64aa1e9
chore(example): update example app for start() rename and Promise-onl…
al-af Aug 2, 2026
cc1b43a
chore(demo): rebrand RN demo app to com.appsflyer.rnpluginexample and…
al-af Aug 2, 2026
9e8c6d5
chore: misc tooling housekeeping
al-af Aug 2, 2026
e78627a
fix(demo): correct rpcCatalog.js against the current appsFlyer API
al-af Aug 2, 2026
4eb7ad4
fix(demo): resolve expo app session-ready bootstrap race
al-af Aug 2, 2026
74489eb
feat(init): report plugin info on initialization
al-af Aug 3, 2026
f9b3887
fix(index): rename listener APIs to match native, simplify types, dro…
al-af Aug 5, 2026
67f0833
refactor: align public API with RPC-to-Plugin-API alignment matrix
al-af Aug 3, 2026
2abb6ac
test: update test suite for RPC-to-Plugin-API alignment renames
al-af Aug 3, 2026
534f727
chore(demo): update expo app for API alignment renames
al-af Aug 3, 2026
c10cba3
chore(demo): update RN demo app for API alignment renames
al-af Aug 3, 2026
9665ea0
chore(example): update example app for API alignment renames
al-af Aug 3, 2026
248063c
docs: document RPC-to-Plugin-API alignment matrix changes
al-af Aug 3, 2026
d7c38d9
fix(index): registerConversionListener's onConversionDataFail is requ…
al-af Aug 5, 2026
1f458be
fix(expo): inject handleLaunchOptions and forward restorationHandler …
al-af Aug 4, 2026
3b25f30
fix(android): normalize deep-link status casing to match iOS
al-af Aug 4, 2026
aa55c12
fix(demo): use Promise-based logEvent, suppress known react-native-el…
al-af Aug 4, 2026
4835c62
docs: consolidate 6.x to 7.0.0 migration guide, deslop integration docs
al-af Aug 4, 2026
2b309bd
feat: Add iOS Apple Ads attribution API and refine consent data handling
al-af Aug 4, 2026
11b4a8a
docs: note #695 rename superseded by #696 register/unregister API in …
al-af Aug 5, 2026
14ac22a
chore: remove internal PR-review tracking doc from the branch (not me…
al-af Aug 5, 2026
8d687df
docs: fix remaining single-arg registerConversionListener examples
al-af Aug 5, 2026
7f9e7fd
refactor: Consolidate plugin API into index.ts and clarify async usage
al-af Aug 5, 2026
9f77e81
fix(ios): Resolve AppsFlyer `continueUserActivity` Swift type ambiguity
al-af Aug 5, 2026
3a97f72
Merge pull request #693 from AppsFlyerSDK/stack/2-turbomodule-core
al-af Aug 6, 2026
572d4e5
Merge pull request #694 from AppsFlyerSDK/stack/3-rpc-contract-fixes
al-af Aug 6, 2026
9ce1cb9
Merge stack/2-turbomodule-core into development (lands #694 commits t…
al-af Aug 6, 2026
2030ec7
Merge pull request #695 from AppsFlyerSDK/stack/4-index-ts-rename
al-af Aug 6, 2026
acbaf03
Merge pull request #696 from AppsFlyerSDK/stack/5-api-alignment
al-af Aug 6, 2026
48a4cad
Merge pull request #697 from AppsFlyerSDK/stack/6-final-polish
al-af Aug 6, 2026
dff744d
patch for Android Build CI
al-af Aug 6, 2026
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
786deae
Merge pull request #699 from AppsFlyerSDK/dev/js-core-migration
al-af Aug 13, 2026
d616cd6
chore(release): bump native SDK deps and package version to 7.0.2
al-af Aug 24, 2026
d8b1574
refactor: delegate mediation-network/fbLoginId handling to js-core-pl…
al-af Aug 24, 2026
44921ac
test: update unit tests and fixtures for js-core-plugin migration
al-af Aug 24, 2026
f03c19f
chore(demo): update demo/example apps for new SDK surface
al-af Aug 24, 2026
552de98
docs: update docs, rules, and release manual for the migration
al-af Aug 24, 2026
87908f2
chore(ci): bump actions/setup-node to v6 and harden the release pipeline
al-af Aug 24, 2026
95bff95
fix(e2e): detect app crash mid-phase in the scenario runner
al-af Aug 24, 2026
abecce5
chore: add npm packaging ignore list, remove dead schema-verify script
al-af Aug 24, 2026
1e27859
chore: bump AppsFlyer PurchaseConnector pod to 7.0.2
al-af Aug 24, 2026
c6d52b2
docs: fix RN_API.md param shapes and add missing method sections
al-af Aug 25, 2026
f7c47ed
fix: PurchaseConnector listeners return EmitterSubscription instead o…
al-af Aug 25, 2026
6aa9d8c
refactor(ios): route handleLaunchOptions through AppsFlyerAttribution…
al-af Aug 25, 2026
45b3baa
chore: bump Android purchase-connector dependency to 2.3.0
al-af Aug 25, 2026
6a805e0
docs: rewrite CHANGELOG/MIGRATION for 7.0.2, clarify setUserFbLoginId…
al-af Aug 25, 2026
2a2dec3
chore: upgrade Node.js to v24 in CI workflows
al-af Aug 25, 2026
78aaee9
chore: upgrade setup-node GitHub action to v7
al-af Aug 25, 2026
65438d7
refactor(demo): Remove redundant Android initial deep link handling
al-af Aug 25, 2026
a6d3cff
docs: standardize AppsFlyer instance casing
al-af Aug 25, 2026
3a18442
docs: refine internal architecture and known issues documentation
al-af Aug 25, 2026
4cc0313
docs: update AppsFlyer listener registration syntax
al-af Aug 25, 2026
2b58cc1
fix: Align AFPurchaseType.ONE_TIME_PURCHASE with publicApi contract
al-af Aug 25, 2026
8113deb
Merge pull request #700 from AppsFlyerSDK/dev/DELIVERY-128515/latest-…
al-af Aug 25, 2026
1762e2f
docs: Align API method examples to object parameter style
al-af Aug 25, 2026
be1c836
chore: Bump js-core-plugin and standardize unregisterDeepLinkListener…
al-af Aug 26, 2026
75dcd1a
chore(release): Finalize 7.0.2 release date
al-af Aug 26, 2026
34a7f57
Merge pull request #701 from AppsFlyerSDK/dev/js-core-bump-7.0.15
al-af Aug 26, 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
39 changes: 25 additions & 14 deletions .af-e2e/test-plan.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,16 @@
"checks": [
{
"id": "sdk_started",
"description": "startSDK was called",
"description": "start was called",
"type": "log_contains",
"pattern": "[AF_QA][startSDK] result:",
"pattern": "[AF_QA][start] result:",
"fail_action": "abort"
},
{
"id": "is_first_launch_true",
"description": "onInstallConversionData fires with is_first_launch=true",
"description": "registerConversionListener fires with is_first_launch=true",
"type": "log_contains",
"pattern": "[AF_QA][CALLBACK][onInstallConversionData]",
"pattern": "[AF_QA][CALLBACK][registerConversionListener]",
"payload_check": {"field": "is_first_launch", "expected": "true"},
"fail_action": "abort"
},
Expand All @@ -65,7 +65,7 @@
"id": "get_sdk_version",
"description": "getSDKVersion returns a value",
"type": "log_contains",
"pattern": "[AF_QA][getSDKVersion] result:",
"pattern": "[AF_QA][getSdkVersion] result:",
"fail_action": "fail"
},
{
Expand Down Expand Up @@ -106,16 +106,16 @@
},
{
"id": "on_deep_linking_callback",
"description": "onDeepLinking fires (NOT_FOUND expected on clean launch)",
"description": "onDeepLinking fires (NOT_FOUND expected on clean launch) — iOS always invokes this callback on cold launch even with no deep link; Android's SDK only invokes it when an actual deep link is present, so this warns instead of fails on Android",
"type": "log_contains",
"pattern": "[AF_QA][CALLBACK][onDeepLinking]",
"fail_action": "fail"
"fail_action": "warn"
},
{
"id": "no_fatal_errors",
"description": "No fatal exceptions or SDK errors in logs",
"type": "absent",
"patterns": ["Fatal Exception", "FATAL", "[AF_QA][startSDK] error:", "response code:4", "response code:5"],
"patterns": ["Fatal Exception", "FATAL", "[AF_QA][start] error:", "response code:4", "response code:5"],
"fail_action": "fail"
}
]
Expand Down Expand Up @@ -293,6 +293,7 @@
"scenario_ref": "E2E-005",
"description": "Fresh install. Verify setCustomerUserId, setCurrencyCode, setAdditionalData propagate correctly. Identity-check event receives HTTP 200. is_first_launch=true still fires.",
"requires_fresh_install": true,
"requires_device_identity_reset": true,
"wait_after_launch_sec": 420,
"checks": [
{
Expand Down Expand Up @@ -326,9 +327,9 @@
},
{
"id": "is_first_launch_true",
"description": "onInstallConversionData still fires with is_first_launch=true",
"description": "registerConversionListener still fires with is_first_launch=true",
"type": "log_contains",
"pattern": "[AF_QA][CALLBACK][onInstallConversionData]",
"pattern": "[AF_QA][CALLBACK][registerConversionListener]",
"payload_check": {"field": "is_first_launch", "expected": "true"},
"fail_action": "fail"
},
Expand All @@ -352,9 +353,19 @@
"checks": [
{
"id": "stop_true",
"description": "stop(true) readback present",
"description": "stop(true) resolves null — the void-RPC success value on both platforms",
"type": "log_contains",
"pattern": "[AF_QA][stop] result: true",
"pattern": "[AF_QA][stop(true)] result: null",
"fail_action": "fail"
},
{
"id": "stop_no_error_payload",
"description": "Neither stop call forwarded an RPC error through its single callback",
"type": "absent",
"patterns": [
"[AF_QA][stop(true)] result: {\"code\"",
"[AF_QA][stop(false)] result: {\"code\""
],
"fail_action": "fail"
},
{
Expand All @@ -366,9 +377,9 @@
},
{
"id": "stop_false",
"description": "stop(false) readback present",
"description": "stop(false) resolves null — the void-RPC success value on both platforms",
"type": "log_contains",
"pattern": "[AF_QA][stop] result: false",
"pattern": "[AF_QA][stop(false)] result: null",
"fail_action": "fail"
},
{
Expand Down
12 changes: 6 additions & 6 deletions .af-smoke/rc-test-plan.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,16 @@
"checks": [
{
"id": "sdk_started",
"description": "startSDK returns a result",
"description": "start returns a result",
"type": "log_contains",
"pattern": "[AF_QA][startSDK] result:",
"pattern": "[AF_QA][start] result:",
"fail_action": "abort"
},
{
"id": "conversion_data",
"description": "onInstallConversionData callback fires",
"description": "registerConversionListener callback fires",
"type": "log_contains",
"pattern": "[AF_QA][CALLBACK][onInstallConversionData]",
"pattern": "[AF_QA][CALLBACK][registerConversionListener]",
"fail_action": "fail"
},
{
Expand Down Expand Up @@ -139,9 +139,9 @@
"checks": [
{
"id": "sdk_started",
"description": "startSDK returns a result on fresh install",
"description": "start returns a result on fresh install",
"type": "log_contains",
"pattern": "[AF_QA][startSDK] result:",
"pattern": "[AF_QA][start] result:",
"fail_action": "abort"
},
{
Expand Down
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
110 changes: 75 additions & 35 deletions .claude/rules/bridge-patterns.md
Original file line number Diff line number Diff line change
@@ -1,60 +1,100 @@
---
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`, and any file that calls `NativeModules.RNAppsFlyer` or `NativeModules.PCAppsFlyer`.
Scope: `index.ts`, `src/NativeAppsFlyer.ts`, `src/rn-transport.ts`. All native calls go through the single TurboModule entry point `NativeAppsFlyer.executeRpc(requestJson)` — no bespoke per-feature native methods.

## 1. Three API patterns coexist
Method dispatch, per-platform wire method-name/param resolution, and event demuxing live in `@appsflyer-sdk/js-core-plugin`, not this repo. This repo's only glue is `src/rn-transport.ts`'s `RNTransport`, implementing `RpcTransport`:

| Pattern | When used | Detection |
|---------|-----------|-----------|
| Dual callback/promise | `initSdk`, `logEvent` | `if (success && error)` routes to `*WithCallBack`; otherwise `*WithPromise` |
| Callback-only | Most config methods (`setCustomerUserId`, `stop`, `setCurrencyCode`) | Optional callback; defaults to `console.log` fallback |
| Event emitter | Deep linking, conversion data, purchase validation | `appsFlyerEventEmitter.addListener(eventName, handler)` |
| `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, match the pattern of similar methods. Do not mix patterns within a single method.
`index.ts` constructs `AppsFlyerSDK` with an `RNTransport` instance, exports it (`export const AppsFlyer = sdk`), and re-exports everything from `@appsflyer-sdk/js-core-plugin` (`export * from "@appsflyer-sdk/js-core-plugin"`).

## 2. Callback-to-native routing
## RPC request/response shape

```js
// Dual pattern — index.js
if (success && error) {
RNAppsFlyer.initSdkWithCallBack(options, success, error);
} else {
return RNAppsFlyer.initSdkWithPromise(options);
}
Every call serializes to:
```json
{ "method": "methodName", "params": { ... } }
```

Every response resolves (never rejects for native-side outcomes) as:
```json
{ "success": true, "data": <any> }
// or
{ "success": false, "error": { "code": <number>, "message": "<string>" } }
```

`RNTransport.call` unwraps this: resolves with `data` on success, rejects with `error` on failure. Android's `error.code` is a distinct number per failure class (e.g. `404` = `RpcErrorCodes.METHOD_NOT_FOUND`) — iOS error codes aren't cross-checked against this numbering, don't assume parity.

The TurboModule Promise itself only rejects if the call never reaches native at all.

## 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). `RNTransport.subscribe` forwards the raw envelope to `@appsflyer-sdk/js-core-plugin`, which owns the demuxing into:
- `onConversionDataSuccess` / `onConversionDataFail`
- `onDeepLinkReceived` (iOS) / `onDeepLinking` (Android) — normalized to one JS-facing shape
- `onSessionReady` — fires once `registerSessionReadyListener` is registered and native signals readiness. `isSessionReady` is a separate one-off Promise query for current state, not a replacement.

The raw `origin`/`timestamp` envelope fields are stripped before handing `data` to app callbacks. No `supportedEvents` array under TurboModules.

## Listener registration order

Registration calls are init-order-independent by design on both platforms — each just assigns a delegate/callback on the persistent native SDK singleton. Only `start`/`logEvent` require `init` to have run first.

Exceptions (native side effects — see `known-issues-kb.md` for root cause):

| Listener | Call order | Why |
|---|---|---|
| `registerSessionReadyListener` | called synchronously (no `.then()`) | avoids a TOCTOU race with `init()` |
| `registerDeepLinkListener` | before `init()`, both platforms | Android drops any pre-registration deep-link result with zero buffering |
| `registerConversionListener` | either order | plain delegate assignment, no side effect |

### Canonical call order

```
registerDeepLinkListener → init → (config setters) → registerConversionListener → registerSessionReadyListener(() => start())
```

The native side has **separate methods** for callback vs promise variants. Adding a new dual method requires implementing both on iOS (`RCT_EXPORT_METHOD`) and Android (`@ReactMethod`).
Register synchronously, not inside `init(...).then()` — deferring into the promise callback delays the one callback that triggers `start()`.

## 3. Event emitter contract
No JS-side buffer exists for these RPCs — don't add one without confirming an actual native regression first (see `known-issues-kb.md`'s "listener-registration buffer removed" entry).

- Events arrive as **JSON strings** from native — always parsed with `JSON.parse` on the JS side
- Parse failures produce `AFParseJSONException` objects (not proper Error subclasses)
- Native must serialize data to JSON string **before** calling `sendEventWithName:body:` (iOS) or `sendEvent` (Android)
- Supported event names are declared in iOS `supportedEvents` and must match exactly on both platforms:
`onAttributionFailure`, `onAppOpenAttribution`, `onInstallConversionFailure`, `onInstallConversionDataLoaded`, `onDeepLinking`, `onValidationResult`
### Deterministic ordering after start()

## 4. Listener registration order
`registerSessionReadyListener`'s callback is the only place to call `start()`. It fires asynchronously, so code written after the registration call in source order runs *before* it, not after. Wrap in a Promise to run app logic strictly after `start()`:

`onDeepLink` (and `onInstallConversionData`, `onAppOpenAttribution`) must be registered **before** `initSdk`. The native SDK fires these callbacks immediately after initialization — if the JS listener isn't attached yet, events are lost silently.
```js
function startWhenSessionReady() {
return new Promise((resolve, reject) => {
AppsFlyer.registerSessionReadyListener(() => {
AppsFlyer.start().then(resolve, reject);
});
});
}
// ... init() + listener registration (not awaited) ...
await startWhenSessionReady();
```

This is the #1 source of GitHub issues (#650, #647, #630, #305, #292). Always validate listener timing in code review.
This only reorders app-controlled code — there's no timeout/fallback if `onSessionReady` never fires.

## 5. No transpilation
`onAppOpenAttribution`, `onAttributionFailure`, `performOnAppAttribution` are removed in 7.0.0 — use `onDeepLinking` instead (see MIGRATION.md).

`index.js` ships as-is via npm — no Babel, no bundler. Write only syntax that Metro and Node can consume directly. The file uses ES module `export` syntax with CommonJS-compatible patterns.
## No transpilation

## 6. Named exports
`index.ts` ships as-is via npm — no Babel, no bundler, no separate `index.js`/`index.d.ts`. Only syntax Metro and Node can consume directly.

Current named exports from `index.js`: `AppsFlyerConsent`, `AFParseJSONException`, `AFPurchaseType`, `MEDIATION_NETWORK`, `StoreKitVersion`, `AppsFlyerPurchaseConnector`, `AppsFlyerPurchaseConnectorConfig`.
## Named exports

Adding a new named export changes the public API surface — requires a minor version bump and matching `index.d.ts` update.
`AFInAppEventType`, `AFPurchaseType`, `MEDIATION_NETWORK`, `StoreKitVersion`, `AppsFlyerPurchaseConnector`, `AppsFlyerPurchaseConnectorConfig`, plus everything `@appsflyer-sdk/js-core-plugin` exports (including `AppsFlyerConsent`). Adding a new named export requires a version bump.

## 7. Default callback fallback
## PurchaseConnector

Many methods use `(result) => console.log(result)` as the default callback when none is provided. This leaks to production logs. Prefer silent no-ops for new methods, or document the logging behavior explicitly.
`PCAppsFlyer` still uses the legacy `NativeModules` bridge — out of scope for the TurboModule rewrite. Don't touch `PurchaseConnector/` when working on RPC/TurboModule changes.
47 changes: 17 additions & 30 deletions .claude/rules/expo-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,56 +5,43 @@ paths:

# Expo config plugin rules

Scope: `expo/` directory — `withAppsFlyer.js`, `withAppsFlyerIos.js`, `withAppsFlyerAndroid.js`.
Scope: `expo/withAppsFlyer.js`, `withAppsFlyerIos.js`, `withAppsFlyerAndroid.js`. These run at `expo prebuild` time to modify native project files; the host app must have New Architecture enabled (not enforced by the plugin itself).

## 1. Config plugin structure
## Structure

```
expo/
├── withAppsFlyer.js ← Entry point, composes iOS + Android plugins
├── withAppsFlyerIos.js ← Modifies AppDelegate for deep link handling
├── withAppsFlyerAndroid.js ← Modifies AndroidManifest.xml
└── withAppsFlyerAppDelegate.js ← AppDelegate code injection
├── withAppsFlyerIos.js ← Modifies AppDelegate (ObjC + Swift) + Podfile
└── withAppsFlyerAndroid.js ← Modifies AndroidManifest.xml
```

These are Expo Config Plugins — they run at `expo prebuild` time to modify native project files.
## Swift AppDelegate support

## 2. Swift AppDelegate problem (critical, unresolved)
`withAppsFlyerIos.js`'s `modifySwiftAppDelegate` string-matches Expo's default Swift AppDelegate template (`didFinishLaunchingWithOptions`/`openURL`/`continueUserActivity`) and injects `handleLaunchOptions`/`handleOpen`/`continueUserActivity` calls via `AppsFlyerAttribution.shared` (one `import react_native_appsflyer`, no `AppsFlyerLib` import needed). `modifyObjcAppDelegate` handles the legacy ObjC template the same way.

Starting with Expo SDK 52 / RN 0.76, the default AppDelegate is **Swift** (not Objective-C). The plugin's `withAppsFlyerAppDelegate.js` modifies ObjC code and **fails silently** on Swift AppDelegates (#638, #620).
Both matchers are exact-string-match against one template shape — if Expo/RN changes the default AppDelegate boilerplate, the matcher silently misses (falls through to `WarningAggregator.addWarningIOS`, not a build failure) instead of adapting. Re-verify the matched strings against a fresh `expo prebuild` output whenever bumping the supported Expo SDK version.

Until this is fixed:
- Do not assume AppDelegate is ObjC in config plugin code
- Test with both `expo prebuild` (Swift default) and legacy ObjC projects
- This is the #1 Expo compatibility blocker
## Manifest merge is not idempotent

## 3. Manifest merge duplication
`withAppsFlyerAndroid.js` appends `tools:replace` entries to `AndroidManifest.xml` without checking for existing entries — repeated `expo prebuild` (without `--clean`) duplicates them and breaks the Android build. Always check if the entry exists before appending.

`withAppsFlyerAndroid.js` appends `tools:replace` entries to `AndroidManifest.xml`. Running `expo prebuild` multiple times (without `--clean`) causes **duplicate entries** that break the Android build (#672).
## Expo Go incompatibility

Fix pattern: always check if the entry exists before appending. Use idempotent modifications.
Requires native modules unavailable in Expo Go — only works in development builds (`eas build --profile development`) or bare workflow.

## 4. Expo Go incompatibility
## No test coverage

The plugin requires native modules unavailable in Expo Go. Only works in development builds (`eas build --profile development`) or bare workflow. This is documented but users miss it repeatedly (#542).
Zero test coverage on the config plugins. Manual test with `expo prebuild --clean` on both platforms after any change.

## 5. No test coverage
## Peer dependency

The Expo config plugins have **zero test coverage**. When modifying these files, manual testing with `expo prebuild --clean` on both platforms is required. Consider adding unit tests that mock the Expo config plugin API.
`expo` is an optional peer dependency — guard all Expo-specific imports/config so the plugin works without Expo installed.

## 6. Peer dependency

`expo` is declared as an optional peer dependency. The plugin must work without Expo installed — guard all Expo-specific imports and config.

## 7. Testing changes
## Testing changes

```bash
# Clean prebuild (recommended)
cd demos/demo && npx expo prebuild --clean

# Verify Android manifest
cat android/app/src/main/AndroidManifest.xml | grep -A5 "appsflyer"

# Verify iOS AppDelegate
cat ios/demo/AppDelegate.m # or AppDelegate.swift for Expo 52+
cat ios/demo/AppDelegate.swift # or AppDelegate.m pre-Expo-52
```
Loading
Loading