From 5bb82d4fca9589046d8b183aa2d3a0d08ca6e8d2 Mon Sep 17 00:00:00 2001 From: Amit Levy Date: Mon, 14 Sep 2026 14:37:54 +0300 Subject: [PATCH 1/5] build: bump af-android-plugin-bridge to 7.0.13 and @appsflyer-sdk/js-core-plugin to 7.1.0 --- .claude/rules/native-android.md | 2 +- README.md | 2 +- android/build.gradle | 2 +- package.json | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.claude/rules/native-android.md b/.claude/rules/native-android.md index 8b1bcc6c..4009251d 100644 --- a/.claude/rules/native-android.md +++ b/.claude/rules/native-android.md @@ -46,7 +46,7 @@ Emitted via `reactApplicationContext.emitDeviceEvent("RNAppsFlyer_rpcEvent", pay ## Build setup -`android/build.gradle` pins `af-android-plugin-bridge:7.0.12` explicitly — `af-android-sdk-bom:7.0.1` doesn't carry a matching version. `namespace` is declared for AGP 8.0+. `minSdkVersion` defaults to 21 — verify `plugin_bridge`'s own `minSdkVersion` is ≤21 before release. +`android/build.gradle` pins `af-android-plugin-bridge:7.0.13` explicitly — `af-android-sdk-bom:7.0.1` doesn't carry a matching version. `namespace` is declared for AGP 8.0+. `minSdkVersion` defaults to 21 — verify `plugin_bridge`'s own `minSdkVersion` is ≤21 before release. `AppsFlyerRpcHandler`'s constructor takes `contextProvider: () -> Context`, not `context: Context` — a named-arg call using `context =` fails to compile. `RNAppsFlyerModule.kt` passes `contextProvider = { reactApplicationContext.currentActivity ?: reactApplicationContext }`, invoked fresh on every call (not cached), so `init()` can backfill a missed `onActivityResumed` transition instead of stalling session-ready. diff --git a/README.md b/README.md index 2560e0bf..e400bdd8 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ To do so, please follow [this article](https://support.appsflyer.com/hc/en-us/ar ### This plugin is built for -- Android AppsFlyer SDK (`af-android-sdk`) **v7.0.1** via `af-android-plugin-bridge` **v7.0.12** +- Android AppsFlyer SDK (`af-android-sdk`) **v7.0.1** via `af-android-plugin-bridge` **v7.0.13** - iOS AppsFlyer SDK **v7.0.2** via `AppsFlyerRPC` **v7.0.13** - Requires React Native **>=0.76.0 with the New Architecture (TurboModules) enabled** — apps not yet on the New Architecture must stay on the `6.x` line of this plugin. See [MIGRATION.md](MIGRATION.md). diff --git a/android/build.gradle b/android/build.gradle index f1aa0519..72eb7862 100755 --- a/android/build.gradle +++ b/android/build.gradle @@ -94,7 +94,7 @@ dependencies { implementation platform('com.appsflyer:af-android-sdk-bom:7.0.1') implementation 'com.appsflyer:af-android-sdk' // pinned explicitly — af-android-sdk-bom (7.0.1) doesn't carry this version yet - implementation 'com.appsflyer:af-android-plugin-bridge:7.0.12' + implementation 'com.appsflyer:af-android-plugin-bridge:7.0.13' testImplementation 'junit:junit:4.13.2' // android.jar's org.json stub throws/returns null for every method under diff --git a/package.json b/package.json index 3201e823..e5e01ee5 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-appsflyer", - "version": "7.0.2", + "version": "7.0.2-v2", "description": "React Native Appsflyer plugin", "main": "index.ts", "types": "index.ts", @@ -47,7 +47,7 @@ "react-native": ">=0.76.0" }, "dependencies": { - "@appsflyer-sdk/js-core-plugin": "7.0.15" + "@appsflyer-sdk/js-core-plugin": "7.1.0" }, "devDependencies": { "@babel/preset-env": "^7.26.9", From e24ae2b8c7663644a4d8543c50c4315d3116269d Mon Sep 17 00:00:00 2001 From: Amit Levy Date: Mon, 14 Sep 2026 14:37:54 +0300 Subject: [PATCH 2/5] feat(android): add setAndroidIdData, setImeiData, and setOaidData API, docs, and tests --- Docs/RN_API.md | 65 +++++++++++++++++++- MIGRATION.md | 11 ++-- __tests__/fixtures/android-rpc-contract.json | 30 +++++++++ __tests__/index.test.js | 39 ++++++++++++ __tests__/rpc-wire-contract.test.js | 3 + demos/appsflyer-expo-app/rpcCatalog.js | 3 + 6 files changed, 145 insertions(+), 6 deletions(-) diff --git a/Docs/RN_API.md b/Docs/RN_API.md index 54b0fb32..39f9d08b 100644 --- a/Docs/RN_API.md +++ b/Docs/RN_API.md @@ -67,6 +67,9 @@ The list of available methods for this plugin is described below. - [enableFacebookDeferredApplinks](#enablefacebookdeferredapplinks) - [Android Only APIs](#android-only-apis) - [setCollectAndroidID](#setcollectandroidid) + - [setAndroidIdData](#setandroididdata) + - [setImeiData](#setimeidata) + - [setOaidData](#setoaiddata) - [setCollectIMEI — removed in 7.0.0](#setcollectimei--removed-in-700) - [setDisableNetworkData `setDisableNetworkData(isDisable)`](#setdisablenetworkdata-setdisablenetworkdataisdisable) - [performDeepLinking](#performdeeplinking) @@ -1199,10 +1202,68 @@ if (Platform.OS == 'android') { --- +### setAndroidIdData +`setAndroidIdData(params) : Promise` + +Explicitly send the Android ID to AppsFlyer. Call before `start()`. + +| parameter | type | description | +| --------- | ------ | ------------------ | +| androidId | string | Android ID to send | + +*Example:* + +```javascript +if (Platform.OS == 'android') { + AppsFlyer.setAndroidIdData({ androidId: 'android-id-value' }); +} +``` + +--- + +### setImeiData +`setImeiData(params) : Promise` + +Explicitly send the device IMEI to AppsFlyer. Call before `start()`. +This is the manual-IMEI setter; `setCollectIMEI` was removed in 7.0.0. + +| parameter | type | description | +| --------- | ------ | ------------ | +| imei | string | IMEI to send | + +*Example:* + +```javascript +if (Platform.OS == 'android') { + AppsFlyer.setImeiData({ imei: 'imei-value' }); +} +``` + +--- + +### setOaidData +`setOaidData(params) : Promise` + +Explicitly send the device OAID to AppsFlyer. Call before `start()`. + +| parameter | type | description | +| --------- | ------ | ------------ | +| oaid | string | OAID to send | + +*Example:* + +```javascript +if (Platform.OS == 'android') { + AppsFlyer.setOaidData({ oaid: 'oaid-value' }); +} +``` + +--- + ### setCollectIMEI — removed in 7.0.0 -Android IMEI-collection opt-out has no RPC equivalent and is **removed** with no adapter -(IMEI collection has also been phased out at the OS level on modern Android versions). See +Android IMEI-collection opt-out has no RPC equivalent and is **removed**. +To pass IMEI yourself, use [setImeiData](#setimeidata). See [MIGRATION.md](../MIGRATION.md#full-api-change-reference). ### setDisableNetworkData diff --git a/MIGRATION.md b/MIGRATION.md index 426a418c..09ff93da 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -8,8 +8,8 @@ return an `EmitterSubscription` instead of a bare unsubscribe function — see [details below](#purchaseconnector-listeners-now-return-emittersubscription). Check `package.json`'s `version` for exactly what you're on — this guide covers the whole 7.0.x -line (currently `7.0.2`, native SDK `AppsFlyerRPC` 7.0.13 on iOS / `af-android-sdk` 7.0.1 + -`af-android-plugin-bridge` 7.0.12 on Android). +line (currently `7.0.2-v2`, native SDK `AppsFlyerRPC` 7.0.13 on iOS / `af-android-sdk` 7.0.1 + +`af-android-plugin-bridge` 7.0.13 on Android). - [Prerequisite](#prerequisite) - [Checklist](#checklist) @@ -32,7 +32,7 @@ critical/security fixes for 6 months from the first 7.0.x release. ## Checklist -1. Enable New Architecture, bump to `^7.0.2`, reinstall native deps. +1. Enable New Architecture, bump to `7.0.2-v2`, reinstall native deps. 2. Fix every call site listed in the [full API change reference](#full-api-change-reference). 3. Replace `initSdk(...)` with the `init` + `start` flow, including moving `registerDeepLinkListener` to *before* `init()`. @@ -132,7 +132,7 @@ placeholder. | `setAdditionalData(data, cb?)` | `setAdditionalData({customData})` | Callback → Promise | | | `setAppInviteOneLinkID(oneLinkID, cb?)` | `setAppInviteOneLink({oneLinkId})` | Renamed + Callback → Promise | | | `setCollectAndroidID(isCollect, cb?)` | `setCollectAndroidID({isCollect})` | Callback → Promise | Android only | -| `setCollectIMEI(...)` | — | Removed, no replacement | IMEI collection is obsolete | +| `setCollectIMEI(...)` | — | Removed | Opt-out has no RPC equivalent. To send IMEI yourself, use `setImeiData({imei})`. | | `setCurrencyCode(code, cb?)` | `setCurrencyCode({currencyCode})` | Callback → Promise | | | `setCustomerUserId(uid, cb?)` | `setCustomerUserId({customerId})` | Callback → Promise | | | `setDisableNetworkData(disable, cb?)` | `setDisableNetworkData({isDisable})` | Callback → Promise; param rename only | Android only | @@ -153,6 +153,9 @@ placeholder. | — | `setUserLastName({lastName})` | Net-new | Hashed-PII setter | | — | `setUserFbLoginId({fbLoginId})` | Net-new | Hashed-PII setter; `fbLoginId` accepts `string \| number` — Facebook login IDs run 15-18 digits, past JS's 53-bit safe-integer range, so pass a string for IDs near that limit (no big-integer transform happens on your behalf) | | — | `clearUserPii()` | Net-new | Clears every PII field set via `setUserEmail`/`setUserPhone`/`setUserFirstName`/`setUserLastName`/`setUserFbLoginId` | +| — | `setAndroidIdData({androidId})` | Net-new | Android only (added in 7.0.2-v2) | +| — | `setImeiData({imei})` | Net-new | Android only (added in 7.0.2-v2) | +| — | `setOaidData({oaid})` | Net-new | Android only (added in 7.0.2-v2) | ## Details on selected changes diff --git a/__tests__/fixtures/android-rpc-contract.json b/__tests__/fixtures/android-rpc-contract.json index cd6f9f34..0a3c7a17 100644 --- a/__tests__/fixtures/android-rpc-contract.json +++ b/__tests__/fixtures/android-rpc-contract.json @@ -382,6 +382,36 @@ } } }, + "setAndroidIdData": { + "parser": "parseSetAndroidIdDataRequest", + "params": { + "androidId": { + "accessor": "optString", + "required": false, + "default": "" + } + } + }, + "setImeiData": { + "parser": "parseSetImeiDataRequest", + "params": { + "imei": { + "accessor": "optString", + "required": false, + "default": "" + } + } + }, + "setOaidData": { + "parser": "parseSetOaidDataRequest", + "params": { + "oaid": { + "accessor": "optString", + "required": false, + "default": "" + } + } + }, "setDisableAdvertisingIdentifiers": { "parser": "parseSetDisableAdvertisingIdentifiersRequest", "params": { diff --git a/__tests__/index.test.js b/__tests__/index.test.js index e68aaac8..a068b0ce 100644 --- a/__tests__/index.test.js +++ b/__tests__/index.test.js @@ -416,6 +416,45 @@ describe("Test AppsFlyer API's", () => { await expect(AppsFlyer.setCollectAndroidID({ isCollect: true })).rejects.toThrow(/not supported on ios/); }); + test('it calls AppsFlyer.setAndroidIdData — Android-only', () => { + const { AppsFlyer: androidAppsFlyer, NativeAppsFlyer: androidNative } = freshAppsFlyerForPlatform('android'); + androidAppsFlyer.setAndroidIdData({ androidId: 'android-id' }); + expect(lastPayload(androidNative.executeRpc)).toEqual({ + method: 'setAndroidIdData', + params: { androidId: 'android-id' }, + }); + }); + + test('setAndroidIdData rejects on iOS — Android-only', async () => { + await expect(AppsFlyer.setAndroidIdData({ androidId: 'android-id' })).rejects.toThrow(/not supported on ios/); + }); + + test('it calls AppsFlyer.setImeiData — Android-only', () => { + const { AppsFlyer: androidAppsFlyer, NativeAppsFlyer: androidNative } = freshAppsFlyerForPlatform('android'); + androidAppsFlyer.setImeiData({ imei: 'imei' }); + expect(lastPayload(androidNative.executeRpc)).toEqual({ + method: 'setImeiData', + params: { imei: 'imei' }, + }); + }); + + test('setImeiData rejects on iOS — Android-only', async () => { + await expect(AppsFlyer.setImeiData({ imei: 'imei' })).rejects.toThrow(/not supported on ios/); + }); + + test('it calls AppsFlyer.setOaidData — Android-only', () => { + const { AppsFlyer: androidAppsFlyer, NativeAppsFlyer: androidNative } = freshAppsFlyerForPlatform('android'); + androidAppsFlyer.setOaidData({ oaid: 'oaid' }); + expect(lastPayload(androidNative.executeRpc)).toEqual({ + method: 'setOaidData', + params: { oaid: 'oaid' }, + }); + }); + + test('setOaidData rejects on iOS — Android-only', async () => { + await expect(AppsFlyer.setOaidData({ oaid: 'oaid' })).rejects.toThrow(/not supported on ios/); + }); + test('it calls AppsFlyer.disableAppSetId — Android-only', () => { const { AppsFlyer: androidAppsFlyer, NativeAppsFlyer: androidNative } = freshAppsFlyerForPlatform('android'); androidAppsFlyer.disableAppSetId(); diff --git a/__tests__/rpc-wire-contract.test.js b/__tests__/rpc-wire-contract.test.js index 669557f8..d2453fcc 100644 --- a/__tests__/rpc-wire-contract.test.js +++ b/__tests__/rpc-wire-contract.test.js @@ -213,6 +213,9 @@ const CALL_SITES = [ // Android-only surface { api: 'setCollectAndroidID', platforms: [ANDROID], invoke: (AppsFlyer) => AppsFlyer.setCollectAndroidID({ isCollect: true }) }, + { api: 'setAndroidIdData', platforms: [ANDROID], invoke: (AppsFlyer) => AppsFlyer.setAndroidIdData({ androidId: 'android-id' }) }, + { api: 'setImeiData', platforms: [ANDROID], invoke: (AppsFlyer) => AppsFlyer.setImeiData({ imei: 'imei' }) }, + { api: 'setOaidData', platforms: [ANDROID], invoke: (AppsFlyer) => AppsFlyer.setOaidData({ oaid: 'oaid' }) }, { api: 'setDisableNetworkData', platforms: [ANDROID], invoke: (AppsFlyer) => AppsFlyer.setDisableNetworkData({ isDisable: true }) }, { api: 'unregisterDeepLinkListener', diff --git a/demos/appsflyer-expo-app/rpcCatalog.js b/demos/appsflyer-expo-app/rpcCatalog.js index 15c8dc04..e67b6aab 100644 --- a/demos/appsflyer-expo-app/rpcCatalog.js +++ b/demos/appsflyer-expo-app/rpcCatalog.js @@ -81,6 +81,9 @@ export const RPC_CATALOG = [ // Android-only { name: 'setCollectAndroidID', group: 'Android', platform: 'android', run: () => AppsFlyer.setCollectAndroidID({ isCollect: true }) }, + { name: 'setAndroidIdData', group: 'Android', platform: 'android', run: () => AppsFlyer.setAndroidIdData({ androidId: 'android-id-value' }) }, + { name: 'setImeiData', group: 'Android', platform: 'android', run: () => AppsFlyer.setImeiData({ imei: 'imei-value' }) }, + { name: 'setOaidData', group: 'Android', platform: 'android', run: () => AppsFlyer.setOaidData({ oaid: 'oaid-value' }) }, { name: 'getHostName', group: 'Android', platform: 'android', run: () => AppsFlyer.getHostName() }, { name: 'getHostPrefix', group: 'Android', platform: 'android', run: () => AppsFlyer.getHostPrefix() }, { name: 'getOutOfStore', group: 'Android', platform: 'android', run: () => AppsFlyer.getOutOfStore() }, From d45e68abf157a492ac36f2cc77738a4acf31d1ba Mon Sep 17 00:00:00 2001 From: Amit Levy Date: Mon, 14 Sep 2026 14:37:54 +0300 Subject: [PATCH 3/5] test: add auto-booting and harden process cleanup in scenario runner and smoke plan --- .af-smoke/rc-test-plan.json | 91 +++++++++++++++++++++++------------ Makefile | 21 +++++--- scripts/af-scenario-runner.sh | 91 +++++++++++++++++++++++++++++------ 3 files changed, 150 insertions(+), 53 deletions(-) diff --git a/.af-smoke/rc-test-plan.json b/.af-smoke/rc-test-plan.json index bdd962a0..36ee3bf4 100644 --- a/.af-smoke/rc-test-plan.json +++ b/.af-smoke/rc-test-plan.json @@ -2,8 +2,8 @@ "_meta": { "plan_id": "reactnative-rc-smoke", "plugin": "reactnative", - "version": "1.0.0", - "description": "Post-publish smoke plan for the AppsFlyer React Native plugin. Exercises SMOKE-001/002/003 against example_rc_smoke/, which pins react-native-appsflyer@ from npm. This plan never depends on plugin source.", + "version": "1.1.0", + "description": "Post-publish smoke plan for the AppsFlyer React Native plugin. Exercises SMOKE-001/002/003 (E2E-001/002/003 subset) against example_rc_smoke/, which pins react-native-appsflyer@ from npm. This plan never depends on plugin source.", "platforms": ["android", "ios"], "schema_version": "1.0.0", "tooling_contract_ref": "SMOKE-001, SMOKE-002, SMOKE-003" @@ -28,33 +28,35 @@ "id": "phase_1", "name": "Cold launch smoke (RC artifact)", "scenario_ref": "SMOKE-001", - "description": "Fresh install using the npm-pinned RC build. Validates SDK startup, install conversion data, pre/post-start API markers, and standard events.", + "description": "Fresh install using the npm-pinned RC build. Validates SDK startup, install conversion data, pre/post-start API markers, and standard events. Shorter wait than E2E-001.", "requires_fresh_install": true, + "requires_device_identity_reset": true, "wait_after_launch_sec": 60, "checks": [ { "id": "sdk_started", - "description": "start returns a result", + "description": "start was called", "type": "log_contains", "pattern": "[AF_QA][start] result:", "fail_action": "abort" }, { - "id": "conversion_data", - "description": "registerConversionListener callback fires", + "id": "is_first_launch_true", + "description": "registerConversionListener fires with is_first_launch=true", "type": "log_contains", "pattern": "[AF_QA][CALLBACK][registerConversionListener]", + "payload_check": {"field": "is_first_launch", "expected": "true"}, "fail_action": "fail" }, { - "id": "pre_start_complete", + "id": "pre_start_apis_complete", "description": "Pre-start auto APIs ran", "type": "log_contains", "pattern": "[AF_QA][AUTO_APIS] --- Pre-start auto APIs complete ---", "fail_action": "fail" }, { - "id": "post_start_complete", + "id": "post_start_apis_complete", "description": "Post-start auto APIs ran", "type": "log_contains", "pattern": "[AF_QA][AUTO_APIS] --- Post-start auto APIs complete ---", @@ -69,10 +71,10 @@ "fail_action": "fail" }, { - "id": "no_fatal", - "description": "No fatal exceptions or process crashes", + "id": "no_fatal_errors", + "description": "No fatal exceptions or SDK errors in logs", "type": "absent", - "patterns": ["FATAL EXCEPTION", "Process crashed"], + "patterns": ["Fatal Exception", "FATAL EXCEPTION", "[AF_QA][start] error:"], "fail_action": "fail" } ] @@ -82,7 +84,7 @@ "id": "phase_2", "name": "Background deep link (RC artifact)", "scenario_ref": "SMOKE-002", - "description": "Backgrounds the RC build after Phase 1. Deep link returns app to foreground. onDeepLinking fires with expected deep link value.", + "description": "App backgrounded after Phase 1, then deep link triggers re-entry. Verifies URL is delivered to native layer. onDeepLinking attribution (FOUND/value) requires real OneLink and is warn-only in CI.", "requires_fresh_install": false, "wait_after_trigger_sec": 15, "deep_link_url": "afqa-reactnative://deeplink?deep_link_value=qa_deeplink_bg&af_sub1=background_test&pid=testmedia&c=deeplink_test", @@ -96,24 +98,38 @@ }, "checks": [ { - "id": "deep_link_received", - "description": "onDeepLinking callback fires", + "id": "deeplink_url_received", + "description": "Deep link URL delivered to native app layer", "type": "log_contains", - "pattern": "[AF_QA][CALLBACK][onDeepLinking]", + "pattern": "[AF_QA][DEEPLINK_NATIVE]", "fail_action": "fail" }, { - "id": "deep_link_value", - "description": "Deep link value matches qa_deeplink_bg", + "id": "deeplink_callback_fired", + "description": "onDeepLinking callback fires after deep link (any status)", "type": "log_contains", - "pattern": "qa_deeplink_bg", + "pattern": "[AF_QA][CALLBACK][onDeepLinking]", "fail_action": "fail" }, { - "id": "no_fatal", + "id": "deeplink_status_found", + "description": "onDeepLinking fires with Status.FOUND (requires real OneLink attribution — expected to warn in CI)", + "type": "log_contains", + "pattern": "status=FOUND", + "fail_action": "warn" + }, + { + "id": "deeplink_value_bg", + "description": "deepLinkValue matches qa_deeplink_bg (requires server-side attribution — expected to warn in CI)", + "type": "log_contains", + "pattern": "deepLinkValue=qa_deeplink_bg", + "fail_action": "warn" + }, + { + "id": "no_fatal_errors", "description": "No fatal exceptions after deep link", "type": "absent", - "patterns": ["FATAL EXCEPTION", "Process crashed"], + "patterns": ["Fatal Exception", "FATAL EXCEPTION"], "fail_action": "fail" } ] @@ -123,7 +139,7 @@ "id": "phase_3", "name": "Foreground deep link (RC artifact)", "scenario_ref": "SMOKE-003", - "description": "Fresh install of the RC build, then deep link while app is in foreground. Verifies SDK start and deep link callback.", + "description": "Fresh install of the RC build. App in foreground after SDK start, then deep link. Attribution checks (FOUND/value) are warn-only in CI.", "requires_fresh_install": true, "wait_after_launch_sec": 60, "wait_after_trigger_sec": 15, @@ -145,24 +161,38 @@ "fail_action": "abort" }, { - "id": "deep_link_received", - "description": "onDeepLinking callback fires after foreground deep link", + "id": "deeplink_url_received", + "description": "Deep link URL delivered to native app layer", "type": "log_contains", - "pattern": "[AF_QA][CALLBACK][onDeepLinking]", + "pattern": "[AF_QA][DEEPLINK_NATIVE]", "fail_action": "fail" }, { - "id": "deep_link_value", - "description": "Deep link value matches qa_deeplink_fg", + "id": "deeplink_callback_fired", + "description": "onDeepLinking callback fires after deep link (any status)", "type": "log_contains", - "pattern": "qa_deeplink_fg", + "pattern": "[AF_QA][CALLBACK][onDeepLinking]", "fail_action": "fail" }, { - "id": "no_fatal", + "id": "deeplink_status_found", + "description": "onDeepLinking fires with Status.FOUND (requires real OneLink attribution — expected to warn in CI)", + "type": "log_contains", + "pattern": "status=FOUND", + "fail_action": "warn" + }, + { + "id": "deeplink_value_fg", + "description": "deepLinkValue matches qa_deeplink_fg (requires server-side attribution — expected to warn in CI)", + "type": "log_contains", + "pattern": "deepLinkValue=qa_deeplink_fg", + "fail_action": "warn" + }, + { + "id": "no_fatal_errors", "description": "No fatal exceptions", "type": "absent", - "patterns": ["FATAL EXCEPTION", "Process crashed"], + "patterns": ["Fatal Exception", "FATAL EXCEPTION"], "fail_action": "fail" } ] @@ -170,6 +200,7 @@ ], "report": { - "output_dir": ".af-smoke/reports" + "output_dir": ".af-smoke/reports", + "format": "json" } } diff --git a/Makefile b/Makefile index a1064940..d8c629cf 100644 --- a/Makefile +++ b/Makefile @@ -34,19 +34,28 @@ _VERBOSE_FLAG := $(if $(VERBOSE),--verbose,) .PHONY: build-ios build-ios: @echo "Building iOS example app..." + @udid="$(IOS_SIMULATOR_UDID)"; \ + if [ -z "$$udid" ]; then \ + udid=$$(xcrun simctl list devices booted 2>/dev/null | grep -oE '[0-9A-F]{8}-([0-9A-F]{4}-){3}[0-9A-F]{12}' | head -1); \ + fi; \ + if [ -z "$$udid" ]; then \ + udid=$$(xcrun simctl list devices available 2>/dev/null | grep -E 'iPhone' | grep -oE '[0-9A-F]{8}-([0-9A-F]{4}-){3}[0-9A-F]{12}' | head -1); \ + if [ -z "$$udid" ]; then echo "error: no available iPhone simulator" >&2; exit 1; fi; \ + echo "No booted iOS simulator; booting $$udid"; \ + xcrun simctl boot "$$udid"; \ + xcrun simctl bootstatus "$$udid" -b; \ + fi; \ cd example/ios && xcodebuild build \ -workspace example.xcworkspace \ -scheme example \ -configuration Debug \ - -destination 'platform=iOS Simulator,id=$(IOS_SIMULATOR_UDID)' \ + -destination "platform=iOS Simulator,id=$$udid" \ -derivedDataPath build \ | tail -3 .PHONY: e2e-ios e2e-ios: - @test -n "$(IOS_SIMULATOR_UDID)" || { echo "Error: no booted iOS simulator found. Boot one first."; exit 1; } - IOS_SIMULATOR_UDID=$(IOS_SIMULATOR_UDID) /bin/bash $(RUNNER) \ - --platform ios --plan $(PLAN) $(_PHASE_FLAG) $(_VERBOSE_FLAG) + /bin/bash $(RUNNER) --platform ios --plan $(PLAN) $(_PHASE_FLAG) $(_VERBOSE_FLAG) .PHONY: e2e-ios-build e2e-ios-build: build-ios e2e-ios @@ -60,9 +69,7 @@ build-android: .PHONY: e2e-android e2e-android: - @test -n "$(ANDROID_SERIAL)" || { echo "Error: no Android device/emulator found. Start one first."; exit 1; } - ANDROID_SERIAL=$(ANDROID_SERIAL) /bin/bash $(RUNNER) \ - --platform android --plan $(PLAN) $(_PHASE_FLAG) $(_VERBOSE_FLAG) + /bin/bash $(RUNNER) --platform android --plan $(PLAN) $(_PHASE_FLAG) $(_VERBOSE_FLAG) .PHONY: e2e-android-build e2e-android-build: build-android e2e-android diff --git a/scripts/af-scenario-runner.sh b/scripts/af-scenario-runner.sh index 799b0ce9..7f74d9e9 100755 --- a/scripts/af-scenario-runner.sh +++ b/scripts/af-scenario-runner.sh @@ -19,8 +19,8 @@ # # Requirements: # - bash 4+, jq -# - Android: ADB in PATH, emulator booted -# - iOS: Xcode CLI tools, simulator booted +# - Android: ADB in PATH (boots the first AVD if none is running) +# - iOS: Xcode CLI tools (boots an iPhone simulator if none is running) # # The script is agent-agnostic: any AI coding assistant (Cursor, Claude Code, # GitHub Copilot, Windsurf) or a human can invoke it from a terminal. @@ -187,7 +187,59 @@ PHASE_RESULTS="[]" # --- Android --- android_get_device() { - adb devices | grep -w "device" | head -1 | awk '{print $1}' + adb devices 2>/dev/null | awk '/[[:space:]]device$/{print $1; exit}' +} + +android_emulator_bin() { + if command -v emulator >/dev/null 2>&1; then + command -v emulator + return + fi + local sdk + for sdk in "${ANDROID_HOME:-}" "${ANDROID_SDK_ROOT:-}" "$HOME/Library/Android/sdk"; do + if [[ -n "$sdk" && -x "$sdk/emulator/emulator" ]]; then + echo "$sdk/emulator/emulator" + return + fi + done + return 1 +} + +android_ensure_device() { + local device + device=$(android_get_device || true) + if [[ -n "$device" ]]; then + log_info "Android device: $device" + return 0 + fi + + local emu avd + emu=$(android_emulator_bin) || { + log_fail "No Android device and no emulator binary (set ANDROID_HOME or put emulator on PATH)" + exit 1 + } + avd="${ANDROID_AVD:-$("$emu" -list-avds 2>/dev/null | head -1)}" + if [[ -z "$avd" ]]; then + log_fail "No Android AVD found. Create one in Android Studio or: avdmanager create avd" + exit 1 + fi + + log_info "No Android device; booting AVD $avd" + "$emu" -avd "$avd" -no-snapshot-save -no-audio >/dev/null 2>&1 & + local emu_pid=$! i boot + for i in $(seq 1 90); do + device=$(android_get_device || true) + boot=$(adb shell getprop sys.boot_completed 2>/dev/null | tr -d '\r' || true) + if [[ -n "$device" && "$boot" == "1" ]]; then + log_info "Android device: $device" + return 0 + fi + sleep 2 + done + log_fail "Timed out waiting for AVD $avd to finish booting" + pkill -P "$emu_pid" 2>/dev/null || true + kill "$emu_pid" 2>/dev/null || true + exit 1 } android_is_installed() { @@ -294,15 +346,28 @@ ios_get_booted_udid() { jq -r '[.devices[][] | select(.state == "Booted")] | first | .udid // empty' } +ios_pick_available_udid() { + xcrun simctl list devices available -j 2>/dev/null | jq -r ' + [.devices[][] | select(.isAvailable == true and (.name | startswith("iPhone")))] | first | .udid // empty + ' +} + ios_ensure_udid() { + if [[ -n "$IOS_UDID" ]]; then + return 0 + fi + IOS_UDID=$(ios_get_booted_udid) if [[ -z "$IOS_UDID" ]]; then - IOS_UDID=$(ios_get_booted_udid) + IOS_UDID=$(ios_pick_available_udid) if [[ -z "$IOS_UDID" ]]; then - log_fail "No booted iOS simulator found. Boot one with: xcrun simctl boot " + log_fail "No iPhone simulator available. Create one in Xcode." exit 1 fi - log_info "Using simulator: $IOS_UDID" + log_info "No booted iOS simulator; booting $IOS_UDID" + xcrun simctl boot "$IOS_UDID" + xcrun simctl bootstatus "$IOS_UDID" -b fi + log_info "Using simulator: $IOS_UDID" } ios_is_installed() { @@ -637,9 +702,9 @@ validate_check() { payload_field=$(echo "$check_json" | jq -r '.payload_check.field // empty') if [[ -n "$payload_field" ]]; then payload_expected=$(echo "$check_json" | jq -r '.payload_check.expected') - if echo "$match" | grep -q "${payload_field}.*${payload_expected}" 2>/dev/null || \ - echo "$match" | grep -q "\"${payload_field}\":.*${payload_expected}" 2>/dev/null || \ - echo "$match" | grep -q "${payload_field}=${payload_expected}" 2>/dev/null || \ + if echo "$match" | grep -q "${payload_field}=${payload_expected}" 2>/dev/null || \ + echo "$match" | grep -q "\"${payload_field}\":${payload_expected}" 2>/dev/null || \ + echo "$match" | grep -q "\"${payload_field}\": ${payload_expected}" 2>/dev/null || \ echo "$match" | grep -q "${payload_field}: ${payload_expected}" 2>/dev/null; then jq -n --arg evidence "$(echo "$match" | tr -d '\000-\037' | head -c 500)" \ '{status: "PASS", evidence: $evidence}' @@ -960,13 +1025,7 @@ main() { fi else if [[ "$PLATFORM" == "android" ]]; then - local device - device=$(android_get_device) - if [[ -z "$device" ]]; then - log_fail "No Android device/emulator found. Start one with: emulator -avd " - exit 1 - fi - log_info "Android device: $device" + android_ensure_device elif [[ "$PLATFORM" == "ios" ]]; then ios_ensure_udid fi From ccc1dea40770a7c3c142671d8c35502fdecd068f Mon Sep 17 00:00:00 2001 From: Amit Levy Date: Mon, 14 Sep 2026 14:37:54 +0300 Subject: [PATCH 4/5] ci: support -vN production republishes in release workflows and documentation --- .claude/rules/release-versioning.md | 2 +- .github/workflows/rc-smoke.yml | 6 +++--- .github/workflows/release.yml | 13 +++++++------ RELEASE_USER_MANUAL.md | 13 ++++++++++++- 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/.claude/rules/release-versioning.md b/.claude/rules/release-versioning.md index 0cabd6f4..afdff9e5 100644 --- a/.claude/rules/release-versioning.md +++ b/.claude/rules/release-versioning.md @@ -65,7 +65,7 @@ Deprecated methods must keep working at runtime. Add a backward-compat test in ` ## Tag convention -All tags use the `v` prefix (`v6.18.0`, `v7.0.2`, ...). `release.yml` tags every production release (`v$VERSION`) and every RC (`v$VERSION-rcN`). +All tags use the `v` prefix (`v6.18.0`, `v7.0.2`, `v7.0.2-v2`, ...). `release.yml` accepts `X.Y.Z-rcN` on the RC path, and `X.Y.Z` or `X.Y.Z-vN` on production. GitHub tags are always `v$VERSION` (so `7.0.2-v2` becomes `v7.0.2-v2`). `-vN` is part of the version identity and is not stripped on promote. ## Native SDK dependency update diff --git a/.github/workflows/rc-smoke.yml b/.github/workflows/rc-smoke.yml index 77696de2..40d730c6 100644 --- a/.github/workflows/rc-smoke.yml +++ b/.github/workflows/rc-smoke.yml @@ -27,7 +27,7 @@ on: workflow_dispatch: inputs: rc_version: - description: 'RC version to smoke, e.g. 6.18.0-rc1 (must exist on npm)' + description: 'RC version to smoke, e.g. 6.18.0-rc1 or 7.0.2-v2 (must exist on npm)' required: true type: string release_branch: @@ -117,8 +117,8 @@ jobs: # published. Dry runs naturally fail the visibility poll below and # emit a skipped check-run. VERSION=$(node -p "require('./package.json').version") - if [[ ! "$VERSION" =~ -rc[0-9]+$ ]]; then - echo "package.json version is not an RC ($VERSION); skipping." + if [[ ! "$VERSION" =~ -(rc|v)[0-9]+$ ]]; then + echo "package.json version is not an RC or -vN tag ($VERSION); skipping." echo "should_run=false" >> "$GITHUB_OUTPUT" echo "skip_reason=not_rc_version" >> "$GITHUB_OUTPUT" echo "rc_version=$VERSION" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 82265bfb..c003d1a1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,7 +44,7 @@ on: - rc - production rn_version: - description: 'Plugin version (e.g., 6.18.0-rc1 for RC, 6.18.0 for production)' + description: 'Plugin version (e.g., 6.18.0-rc1 for RC, 6.18.0 or 7.0.2-v2 for production)' required: true type: string ios_sdk_version: @@ -610,12 +610,12 @@ jobs: echo "Extracted version from package.json: $VERSION" fi - # Validate version format (X.Y.Z, no -rc suffix for production) - if [[ $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + # Validate version format (X.Y.Z or X.Y.Z-vN; no -rc suffix for production) + if [[ $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+(-v[0-9]+)?$ ]]; then echo "Valid production version format: $VERSION" echo "version=$VERSION" >> "$GITHUB_OUTPUT" else - echo "::error::Invalid production version format: $VERSION (expected X.Y.Z)" + echo "::error::Invalid production version format: $VERSION (expected X.Y.Z or X.Y.Z-vN)" exit 1 fi @@ -728,7 +728,7 @@ jobs: if [[ "$TAG" == "rc" ]]; then npm publish --tag rc --provenance else - npm publish --provenance + npm publish --tag latest --provenance fi echo "Published $VERSION to npm" @@ -1242,7 +1242,8 @@ jobs: CI_JIRA_DOMAIN: ${{ secrets.CI_JIRA_DOMAIN }} run: | set +e - JIRA_FIX_VERSION="React Native SDK v$VERSION" + BASE_VERSION="${VERSION%%-v*}" + JIRA_FIX_VERSION="React Native SDK v$BASE_VERSION" echo "Looking for Jira tickets with fix version: $JIRA_FIX_VERSION" diff --git a/RELEASE_USER_MANUAL.md b/RELEASE_USER_MANUAL.md index 8686c422..879bbb63 100644 --- a/RELEASE_USER_MANUAL.md +++ b/RELEASE_USER_MANUAL.md @@ -40,7 +40,7 @@ release.yml (RC) ── lint-test-build.yml ───┐ | Input | Required | Example | Description | |-------|----------|---------|-------------| | `release_type` | Yes | `rc` | Release type (`rc` or `production`). | -| `rn_version` | Yes | `6.18.0-rc1` | Plugin version. Must match `X.Y.Z-rcN` format for RC. | +| `rn_version` | Yes | `6.18.0-rc1` | Plugin version. RC: must match `X.Y.Z-rcN`. Production: `X.Y.Z` or `X.Y.Z-vN` (e.g. `7.0.2-v2`). | | `ios_sdk_version` | No | `6.18.0` | iOS native AppsFlyer SDK version to pin — RC only. | | `android_sdk_version` | No | `6.18.0` | Android native AppsFlyer SDK version to pin — RC only. | | `android_plugin_bridge_version` | No | `6.18.0` | Android `af-android-plugin-bridge` version to pin — RC only, required alongside `android_sdk_version` (enforced at RC-validation time, not by the form). | @@ -103,6 +103,17 @@ When the PR merges to master: 3. Creates a GitHub release (not pre-release). 4. Notifies Slack. +### Out-of-band or republish production release (-vN) + +When republishing a fix where the base version is already on npm (e.g. `7.0.2-v2`): + +1. Ensure `master` has `package.json` at `X.Y.Z-vN` with `CHANGELOG.md` updated. +2. In GitHub Actions, select the **Release** workflow. +3. Click **Run workflow** and set: + - `release_type`: `production` + - `rn_version`: `X.Y.Z-vN` (e.g. `7.0.2-v2`) + - `dry_run`: `false` (or `true` to test first) + ## 6. Post-release verification ```bash From 057b8a09b3a4440287d90d6277dd5661a62055d4 Mon Sep 17 00:00:00 2001 From: Amit Levy Date: Mon, 14 Sep 2026 14:37:54 +0300 Subject: [PATCH 5/5] chore(release): record 7.0.2-v2 changelog and update native version-sync test --- CHANGELOG.md | 7 +++++++ __tests__/version-sync.test.js | 13 +++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7959208f..13dce6f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 7.0.2-v2 + Release date: *2026-09-14* + +- React Native >> `@appsflyer-sdk/js-core-plugin` 7.1.0 +- React Native >> Android `af-android-plugin-bridge` 7.0.13 +- React Native >> Added Android ID setters `setAndroidIdData`, `setImeiData`, and `setOaidData` + ## 7.0.2 Release date: *2026-08-26* diff --git a/__tests__/version-sync.test.js b/__tests__/version-sync.test.js index ab972e60..3f859b9f 100644 --- a/__tests__/version-sync.test.js +++ b/__tests__/version-sync.test.js @@ -5,20 +5,25 @@ const fs = require('fs'); const path = require('path'); const packageVersion = require('../package.json').version; +const unsuffixedVersion = (packageVersion.match(/^(\d+\.\d+\.\d+)/) || [])[1]; -test('ios/RNAppsFlyer.h kAppsFlyerPluginVersion matches package.json version', () => { +function expectNativeVersion(actual) { + expect([packageVersion, unsuffixedVersion]).toContain(actual); +} + +test('ios/RNAppsFlyer.h kAppsFlyerPluginVersion matches package.json version or unsuffixed X.Y.Z', () => { const iosFile = fs.readFileSync(path.join(__dirname, '..', 'ios', 'RNAppsFlyer.h'), 'utf8'); const match = iosFile.match(/kAppsFlyerPluginVersion\s*=\s*@"([^"]+)"/); expect(match).not.toBeNull(); - expect(match[1]).toBe(packageVersion); + expectNativeVersion(match[1]); }); -test('RNAppsFlyerConstants.kt PLUGIN_VERSION matches package.json version', () => { +test('RNAppsFlyerConstants.kt PLUGIN_VERSION matches package.json version or unsuffixed X.Y.Z', () => { const androidFile = fs.readFileSync( path.join(__dirname, '..', 'android', 'src', 'main', 'java', 'com', 'appsflyer', 'reactnative', 'RNAppsFlyerConstants.kt'), 'utf8' ); const match = androidFile.match(/PLUGIN_VERSION\s*=\s*"([^"]+)"/); expect(match).not.toBeNull(); - expect(match[1]).toBe(packageVersion); + expectNativeVersion(match[1]); });