> ## Documentation Index
> Fetch the complete documentation index at: https://docs.appstack.tech/llms.txt
> Use this file to discover all available pages before exploring further.

# React Native SDK changelog

> Every release of the Appstack React Native SDK, newest first.

Releases of the `react-native-appstack-sdk` npm package, newest first. Always integrate against the latest
stable release — [`appstack-cli review`](/tooling/cli) will tell you what you
are on.

<Card title="React Native integration guide" icon="book-open" href="/SDKs/react-native">
  Install, configure, and send your first events.
</Card>

<Update label="3.2.0" description="2026-09-04" tags={["Added"]}>
  **Added**

  * **iOS:** Added experimental React Native 0.87 Swift Package Manager support. The hand-written manifest splits the Swift bridge from the Objective-C++ TurboModule, wires React Native's generated code and header products, and resolves the exact native Appstack SDK version from its public Swift package. CocoaPods support is unchanged.

  [Release notes on GitHub](https://github.com/appstack-tech/react-native-appstack-sdk/releases/tag/3.2.0)
</Update>

<Update label="3.1.0" description="2026-09-03" tags={["Added","Changed","Fixed"]}>
  **Added**

  * **Custom event parameters are encrypted on the device before they are sent.** Values such as an email address, phone number or name are encrypted in the app rather than on arrival. Parameter names that must stay readable — among them `currency`, `revenue` and campaign fields — are excluded, and that list is controlled by Appstack's backend. No JavaScript change is required and `sendEvent` call sites are unaffected.
    * On iOS this requires iOS 17 or later; on iOS 15 and 16 values are encrypted server-side as before. On Android it works down to the SDK's minimum API level 21.
    * Revenue is read out of the parameters before encryption, so revenue reporting is unchanged, as are purchase `transaction_details` and deeplink user data.
    * A value that cannot be encrypted is omitted from the event, and the remaining parameters are still sent.
    * On Android it adds no new dependency and grows the release AAR by roughly 36 KB. On iOS it is built into the vendored framework.
  * **iOS:** Events carry a diagnostic recording whether Apple Ads attribution was enabled and whether the AdServices token fetch is pending, succeeded or failed. The SDK sends this diagnostic automatically once token resolution completes, rather than on the next `sendEvent` (native `4.5.2`). It has no JavaScript API.
  * **iOS:** The vendored framework ships an Apple privacy manifest declaring its required-reason API usage (native `4.5.1`). No additions to the app's own `PrivacyInfo.xcprivacy` are required.

  **Changed**

  * **iOS:** Updated `AppstackSDK.xcframework` to `4.6.0`, which brings the on-device parameter encryption above along with the parameter and attribution fixes below.
  * **Android:** Updated the native Appstack Android SDK dependency to `1.8.0`, which brings the on-device parameter encryption above.
  * A manual `sendEvent('ASA_ATTRIBUTION')` is now dropped rather than sent as a custom event. iOS `4.6.0` adds `ASA_ATTRIBUTION` to its native event enum and emits it automatically once AdServices token resolution completes, so it joins `INSTALL`, `FIRST_OPEN` and `FIRST_OPEN_GUARDED` in the set the wrapper refuses to forward. It is not part of the public `EventType` API on either platform.

  **Fixed**

  * **iOS:** A `null` nested inside a parameter no longer drops the event. Null values are omitted from the payload and nulls inside arrays are preserved, matching Android. Top-level `null` and `undefined` have been stripped in JavaScript since 3.0.0, but that stripping is shallow, so a null inside an object or array still reached native, where the event was discarded.
  * **iOS:** A parameter holding a value JSON cannot represent no longer crashes the app. This covers `NaN` and infinite numbers, a `Date`, `URL`, `Set` or class instance. Such keys are dropped individually and named in an error log, and the event is sent with its remaining parameters. `NaN` and `Infinity` are typed as `number`, so they satisfy the `JsonValue` type on `parameters`.
  * **iOS:** A single `null` in the attribution match response no longer discards the response. One null query parameter could previously cost an install its attribution data, which surfaced in JavaScript as an empty `getAttributionParams()` result.

  [Release notes on GitHub](https://github.com/appstack-tech/react-native-appstack-sdk/releases/tag/3.1.0)
</Update>

<Update label="3.0.0" description="2026-08-21" tags={["Changed","Removed","Added","Fixed"]}>
  **Changed**

  * **TurboModule migration.** The SDK is now a real TurboModule generated by React Native's codegen, with full backwards compatibility for the legacy architecture. Previously the package shipped no `codegenConfig` at all, so codegen never ran and the module was reached through React Native's legacy interop layer even on the New Architecture. The public JavaScript API is unchanged.
    * Added a `codegenConfig` block (`RNAppstackSdkSpec`) and a real TurboModule spec at `src/NativeAppstackReactNative.ts`; the JavaScript entry point now resolves the native module through `TurboModuleRegistry`, which works on both architectures.
    * **iOS:** `AppstackReactNative` conforms to the generated `NativeAppstackReactNativeSpec` protocol and implements `getTurboModule:` under `RCT_NEW_ARCH_ENABLED`, falling back to `RCTBridgeModule` on the legacy architecture.
    * **Android:** the module now extends the generated `NativeAppstackReactNativeSpec` on the New Architecture (with a `BaseReactPackage` that reports `isTurboModule`), and keeps the `ReactContextBaseJavaModule` implementation on the legacy architecture. Shared logic moved to `AppstackReactNativeModuleImpl`.
  * **Breaking:** `configure()` now rejects a fractional `logLevel`. `logLevel: 1.5` previously passed validation and was silently truncated to `1` by the native casts (`NSInteger` on iOS, `Double.toInt()` on Android); only the documented levels `0`, `1`, `2` and `3` are accepted. The error message is now `logLevel must be one of 0, 1, 2, or 3` (was `logLevel must be a number between 0 and 3`, which read as though `1.5` were valid).
  * **Breaking:** `sendEvent` resolves `void` instead of `true`. The old `true` was misleading — it only ever meant "the call reached native", and was returned even when the native SDK went on to drop the event (automatic-only type, SDK disabled, buffer full, no connectivity). Nothing about delivery is observable from JavaScript, so the return value no longer implies it. Code branching on the result should stop doing so; `await`ing still works unchanged.
  * **`sendEvent` now resolves the event type in JavaScript**, sending native an explicit `(type, name)` pair: a standard type with a `null` name, or the `CUSTOM` category with a name. This fixes a cross-platform divergence in which the *same call* behaved differently per platform: `sendEvent('MY_CUSTOM_EVENT')` — an unknown type with no separate name — was sent by iOS as a custom event named `MY_CUSTOM_EVENT`, while Android collapsed it to `CUSTOM` and then rejected it with `INVALID_EVENT_NAME` because no name had been supplied. Both platforms now receive identical arguments, and the native wrappers' guessing branches are no longer reachable from JavaScript.
  * **A manual `INSTALL` is dropped before reaching native**, logging an error and resolving normally rather than throwing. Installs are recorded automatically and sending them by hand inflates install counts, which is why iOS already discarded them natively. The same guard covers the SDK's internal lifecycle events, which are not part of the public `EventType` API: those exist only in iOS's enum, so sending one by hand previously became a bogus *custom* event on Android alone. Resolving rather than rejecting keeps callers who harmlessly send a manual `INSTALL` today — a silent discard since 2.5.0 — from suddenly seeing failures.
  * **Parameter values of `null` or `undefined` are stripped before the call reaches native.** Android already filtered them out (`filterValues { it != null }`) while iOS forwarded `NSNull`, so identical JavaScript produced a different payload per platform. Stripping is shallow, matching Android's behaviour; an empty result is sent as no parameters at all. `0`, `false` and `''` are real values and are preserved.
  * **`parameters` is typed against a JSON-safe value type** (`Record<string, JsonValue | undefined>`) instead of `Record<string, any>`. Values outside that set — a `Date`, a class instance, a function — never survived the bridge intact; this makes that a compile error instead of a runtime surprise. `AppstackEventParameters` and `JsonValue` are exported for annotating your own payloads.
  * **Android:** the native wrapper now treats an unrecognised event type as a custom event named after that type, matching iOS, instead of rejecting with `INVALID_EVENT_NAME`. Defence in depth only — the JavaScript wrapper no longer sends an unrecognised type, so this path is unreachable from JS; it exists so the divergence above cannot return if that invariant is ever broken.

  **Removed**

  * **Breaking:** the positional `configure(apiKey, isDebug?, endpointBaseUrl?, logLevel?, customerUserId?)` signature, deprecated in 2.6.0, is gone. Only `configure(apiKey, { logLevel, customerUserId })` remains. `isDebug` and `endpointBaseUrl` are removed outright — neither was ever forwarded to the native SDKs, so no behaviour is lost by dropping them. A 2.x-style call now throws an error naming the replacement instead of silently ignoring the extra arguments, which would otherwise discard the `logLevel` and `customerUserId` that follow them.
    ```js theme={null}
    // 2.x
    await AppstackSDK.configure(apiKey, false, undefined, 0, 'user_123');
    // 3.0
    await AppstackSDK.configure(apiKey, { logLevel: 0, customerUserId: 'user_123' });
    ```
  * **Breaking:** `sendEvent(eventType, eventName, parameters)` is now `sendEvent(event, parameters)`. The middle `eventName` argument is gone. Pass a standard `EventType` for a standard event, or your own name for a custom one — there is no separate "custom" mode:
    ```js theme={null}
    // before                                          // after
    sendEvent('PURCHASE', null, { revenue: 4.99 })  →  sendEvent(EventType.PURCHASE, { revenue: 4.99 })
    sendEvent('CUSTOM', 'user_attributes', params)  →  sendEvent('user_attributes', params)
    sendEvent('CUSTOM', 'APP_OPENED')               →  sendEvent('APP_OPENED')
    ```
    A three-argument call now rejects with a message naming the replacement, rather than silently binding the event name to `parameters` and shipping an event whose payload is its own name. (`sendEvent` is `async`, so every validation failure rejects the returned promise rather than throwing synchronously.) The two-argument `sendEvent('CUSTOM', 'my_event')` form — a custom event with no parameters — is rejected on the same grounds, since its name would otherwise land in `parameters`. `EventType` itself stays, and remains the recommended form; a plain string is accepted and resolved case-insensitively.
  * **Breaking:** `EventType.CUSTOM` was removed. In the two-argument API you pass a custom event's name directly, so `CUSTOM` had no caller-facing meaning and was actively a footgun: `sendEvent(EventType.CUSTOM, params)` would have resolved as "standard type CUSTOM with no name", which iOS drops outright and Android sends with a null `event_name`. Removing it turns that into a compile error; the literal string `'CUSTOM'` is also rejected at runtime, with a message pointing at the custom-name form.
  * **Breaking:** `isDebug` and `endpointBaseUrl` are removed from the exported `AppstackConfig` type.
  * **iOS:** The native module no longer subclasses `RCTEventEmitter`. It never emitted any events (`supportedEvents` returned an empty array), and the inherited `addListener` / `removeListeners` methods were already unavailable on the New Architecture. Any code calling `new NativeEventEmitter(NativeModules.AppstackReactNative)` on the legacy architecture should be removed.
  * The two deprecation `console.warn`s that 2.6.0 added for positional `isDebug` / `endpointBaseUrl` are gone with the signature they warned about.
  * Deleted a stale, unwrapped duplicate of two `AppstackSDK.framework` slices that was committed directly under `ios/` (plus its orphaned `Info.plist`). It was not referenced by the podspec but was published in every npm tarball. The vendored framework at `ios/AppstackSDK.xcframework` is unaffected.

  **Added**

  * In development builds (`__DEV__`), `sendEvent` logs a warning when the event string is not a recognised standard type, naming the custom event it became. This is the only place a misspelled standard event is catchable: a typo like `'PURCAHSE'` otherwise becomes a custom event with no error anywhere, silently losing the standard-event semantics that enhanced app campaigns optimise against. It is a heuristic — it also fires on legitimate custom names — so it is advisory, dev-only, and never gates the send.

  **Fixed**

  * **iOS:** The SDK now builds when the host app forces our pod below iOS 15.0, instead of failing to compile. `AppstackASAAttribution` in the vendored framework requires iOS 15.0, and the podspec asks for a 15.0 deployment target — but React Native 0.74 writes its own minimum (13.4) onto every pod target after install, and that setting wins over the podspec. The build then stopped with `'AppstackASAAttribution' is only available in iOS 15.0 or newer`. The two Apple Search Ads calls are now behind a runtime `if #available(iOS 15.0, *)` check. Nothing changes for apps: the Objective-C layer already rejects both methods with `UNSUPPORTED_IOS_VERSION` below iOS 15.0, and Apple Search Ads attribution cannot work there anyway. iOS 15.0 remains the SDK's requirement.
  * **iOS:** The podspec depended on `React-Codegen`, which is an unrelated third-party pod on the public CocoaPods trunk (React Native's pod is `ReactCodegen`). Dependencies are now installed via React Native's own `install_modules_dependencies` helper, which also supplies the codegen and static-framework header search paths that cannot be reproduced by hand. As a side effect, the React-Core header search path fix from 2.1.0 now applies on both architectures instead of only the legacy one.
  * **Android:** Removed `cmakeListsPath: null` from `react-native.config.js`. It never disabled codegen autolinking as its comment claimed (`null` is falsy, so autolinking always fell through to the default generated path), and it obscured the real reason codegen never ran.
  * Removed three hand-written files under `android/src/main/jni/` that imitated codegen output (`AppstackReactNativeSpec.{h,cpp}` and a `CMakeLists.txt` declaring a spec target). They contained no TurboModule code, were never built, and are now replaced by genuine generated artifacts.

  [Release notes on GitHub](https://github.com/appstack-tech/react-native-appstack-sdk/releases/tag/3.0.0)
</Update>

<Update label="2.6.0" description="2026-08-11" tags={["Added","Changed","Fixed","Compatibility"]}>
  **Added**

  * `configure(apiKey, { logLevel, customerUserId })` — an options-object form that removes the need to skip the two deprecated positional parameters. Previously, setting `logLevel` or `customerUserId` meant writing `configure(key, undefined, undefined, 0, 'user_123')`; it is now `configure(key, { logLevel: 0, customerUserId: 'user_123' })`. This matches the named-parameter shape the Flutter plugin already exposes.
  * `AppstackSDK.setCustomerUserId(customerUserId)` — sets or clears the customer user ID after `configure()`, for when the ID is only known once the user logs in. Calling `configure` a second time does not work, because a repeat `configure` is a no-op and ignores its `customerUserId`. It applies to every event sent from that point on; on iOS it also reaches events already buffered but not yet delivered. Clear it on logout so the previous user's ID stops being attached to later events. `null`, `undefined` and `''` all clear the stored ID — unlike `configure`, which rejects an empty string because it never clears. Safe to call at any time; last write wins.
  * **iOS:** `getAttributionParams()` now always resolves with an `appstack_match_status` key telling you whether the attribution request succeeded: `matched`, `matched_no_params`, `organic`, `skipped`, `failed` or `not_configured`. Only `failed` is worth re-reading later; the rest are settled answers. Android does not report this key yet, so check for it rather than assuming both platforms return it.
  * **iOS:** Attribution match outcomes are now visible in the SDK's console output at `logLevel` `0` (debug).

  **Changed**

  * **iOS:** Updated `AppstackSDK.xcframework` to `4.5.0`, which adds the native setter behind `setCustomerUserId` along with the attribution match status.
  * **Android:** Updated the native Appstack Android SDK dependency to `1.7.0`, which adds the native setter behind `setCustomerUserId`.
  * **iOS:** An empty result from `getAttributionParams()` no longer means "not attributed" — read `appstack_match_status` instead.
  * Passing `isDebug: true`, or any `endpointBaseUrl`, positionally now logs a `console.warn` pointing at the options object; their no-op values (`false` and `undefined`) stay silent. Both arguments remain accepted and continue to be ignored — neither has ever been forwarded to the native SDKs.

  **Fixed**

  * **iOS:** `deleteUserData()` now also clears the stored customer user ID.
  * **iOS:** A blank customer user ID is treated as absent instead of being sent as an empty string.
  * **iOS:** A `setCustomerUserId` call made immediately after `configure()` is no longer overwritten by the value passed to `configure()`.

  **Compatibility**

  * The positional signature `configure(apiKey, isDebug?, endpointBaseUrl?, logLevel?, customerUserId?)` is unchanged and still type-checks; the two forms are TypeScript overloads and are distinguished at runtime by whether the second argument is an object. All existing validation and error messages are preserved.

  [Release notes on GitHub](https://github.com/appstack-tech/react-native-appstack-sdk/releases/tag/2.6.0)
</Update>

<Update label="2.5.0" description="2026-08-06" tags={["Changed","Fixed"]}>
  **Changed**

  * **iOS:** Updated `AppstackSDK.xcframework` to `4.4.1`. Attribution matching now includes additional network context to improve match diagnostics.
  * **Android:** Updated the native Appstack Android SDK dependency to `1.6.0`. Attribution matching now includes additional device and network context to improve match accuracy, and install attribution resolves more reliably on first launch. No additional app permissions are required.

  **Fixed**

  * **iOS & Android:** `sendEvent` now ignores `INSTALL`, which the SDKs already track automatically. Sending it by hand previously double-counted installs; such calls are now discarded.

  [Release notes on GitHub](https://github.com/appstack-tech/react-native-appstack-sdk/releases/tag/2.5.0)
</Update>

<Update label="2.4.0" description="2026-07-21" tags={["Changed"]}>
  **Changed**

  * **iOS:** Updated `AppstackSDK.xcframework` to `4.4.0`. Adds Mac Catalyst support to the prebuilt XCFramework and improves install detection and attribution reliability.
  * **Android:** Updated the native Appstack Android SDK dependency to `1.5.0`.

  [Release notes on GitHub](https://github.com/appstack-tech/react-native-appstack-sdk/releases/tag/2.4.0)
</Update>
