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

# React Native version 3.0+

> Migrate to version 3.0 of the Knot React Native SDK: meet the React Native 0.76 and iOS 15.1 requirements, update Jest mocks, and review the behaviour changes.

## Overview

`react-native-knotapi` 3.0.0 is a native TurboModule: React Native's codegen generates its
interface, and it no longer relies on React Native's legacy-module interop layer. The JS API you
call (`Knot`, `addKnotListener`, the `knot:*` events and the payload types) is unchanged. What
changes is the set of versions it builds on, how it is mocked in Jest, and some event behaviour
that 2.x got wrong or that differed between iOS and Android.

## Requirements

| | 2.x | 3.0.0 |
| - | - | - |
| React Native | 0.73 or later | **0.76 or later** (`peerDependencies`: `>=0.76.0`) |
| Architecture | either (New Architecture through React Native's interop layer) | either. On React Native 0.76–0.81 the old and the New Architecture both work; 0.82 and later only have the New Architecture |
| iOS deployment target | 14.0 (on 2.2.1 the `KnotAPI` pod it depends on sets it) | **15.1** |
| Android `minSdkVersion` | 24 | 24 |
| Android `compileSdkVersion` | 35 | 35 |
| JDK to build Android | 17 | 17 (the module now also sets Java 17 itself) |

The React Native 0.76 template already sets iOS 15.1 (`min_ios_version_supported`),
`minSdkVersion` 24 and `compileSdkVersion` 35, and React Native's Gradle plugin compiles every
Android module for Java 17 from 0.73 on. So an app on 0.76+ that kept the template's values changes
nothing here.

Not supported, as in 2.x: Expo Go (the SDK ships native code), Mac Catalyst and visionOS (the
bundled `KnotAPI.xcframework` has only iOS device and simulator slices). Expo apps that use a
[development build / prebuild](https://docs.expo.dev/workflow/prebuild/) are supported.

If you can't meet these yet, see [Staying on 2.x](#staying-on-2-x).

## Upgrade steps

1. **On React Native below 0.76, upgrade React Native first**, with the
   [React Native Upgrade Helper](https://react-native-community.github.io/upgrade-helper/). Until
   then, stay on 2.x.

2. **Install 3.0.0:**

   ```bash theme={"system"}
   yarn add react-native-knotapi@^3
   ```

3. **iOS.**
   * **CocoaPods:** in `ios/Podfile`, use `platform :ios, min_ios_version_supported` (15.1 on
     React Native 0.76+). If you hard-coded `platform :ios, '14.0'` for 2.x on React Native
     0.73–0.75, put `min_ios_version_supported` back: on 0.76+ React Native's own pods need 15.1
     too, so `pod install` fails with "required a higher minimum deployment target" until you do.
   * Set the app target's **Minimum Deployments** to iOS 15.1 or later in Xcode
     (`IPHONEOS_DEPLOYMENT_TARGET`). This applies to CocoaPods and SwiftPM mode alike.
   * Reinstall the native dependencies. With CocoaPods (the default):

     ```bash theme={"system"}
     cd ios && pod install
     ```

     Coming from 2.2.1, the `KnotAPI` pod disappears from `Podfile.lock`. That's expected: the
     native Knot iOS SDK has been bundled inside this package since 2.3.0.

     In React Native's SwiftPM mode (React Native 0.87+, experimental), run this instead of
     `pod install`:

     ```bash theme={"system"}
     cd ios && npx react-native spm
     ```
   * Delete Xcode's DerivedData for the app (Xcode → Settings → Locations, or
     `rm -rf ~/Library/Developer/Xcode/DerivedData/<YourApp>-*`), so no build product from the
     2.x module is reused.

4. **Android.** Build with JDK 17 (`JAVA_HOME`, or Android Studio's Gradle JDK setting), then
   clean once:

   ```bash theme={"system"}
   cd android && ./gradlew clean
   ```

   If your Android host doesn't apply the React Native Gradle plugin, see
   [Android hosts without `com.facebook.react`](#android-hosts-without-com-facebook-react).

5. **Rebuild and reinstall the app** (`npx react-native run-ios` / `run-android`, or your usual
   build). A Metro reload isn't enough after a native module changes.

6. **Check your code:**
   * Jest tests that mock `NativeModules.Knotapi`: use [`react-native-knotapi/jest`](#jest-mocks).
   * Importing the default export (`import Knotapi from 'react-native-knotapi'`): see
     [The default export](#the-default-export).
   * Reading `metaData`, error codes or `onExit`: see [Behaviour changes](#behaviour-changes).

## Code changes

### Jest mocks

In 3.0.0 the module is looked up through `TurboModuleRegistry`, which reads React Native's own
`NativeModules` object, not the `react-native` export your test mocks. So a 2.x mock that returns a
new `NativeModules` from `jest.mock('react-native', …)` no longer reaches the module, and importing
`react-native-knotapi` throws the not-linked error below. A hand-written `react-native` mock that
doesn't spread `jest.requireActual('react-native')` fails earlier, with
`TypeError: Cannot read properties of undefined (reading 'get')`, because it has no
`TurboModuleRegistry`. A mock that instead sets
`NativeModules.Knotapi` on React Native's actual module still happens to work, through React
Native's fallback to legacy modules under Jest. Either way, mock the package itself with the mock
it ships, in a test file or your Jest setup file:

```javascript theme={"system"}
jest.mock('react-native-knotapi', () => require('react-native-knotapi/jest'));
```

The mock exports the same names as the package:

* `Knot.open`, `Knot.openWithoutDelay` and `Knot.close` are `jest.fn()`;
* `Knot.getSdkVersion` is a `jest.fn()` that returns `undefined`;
* `addKnotListener` is a `jest.fn()` that returns `{ remove: jest.fn() }`;
* the default export has `open`, `close`, `getSdkVersion` (returns `null`), `addListener` and
  `removeListeners` as `jest.fn()`.

Assert on these, for example `expect(Knot.open).toHaveBeenCalledWith(...)`.

### The default export

In 2.x the default export was React Native's `NativeModules.Knotapi`, typed `any`. In 3.0.0 it is
the TurboModule itself, typed with the module's codegen `Spec`: `open(params)`, `close()`,
`getSdkVersion(): string | null`, `addListener(eventName)` and `removeListeners(count)`.

Prefer the named exports, `Knot` and `addKnotListener`: they are the documented API, and
`Knot.getSdkVersion()` returns `undefined` where the module returns `null`. If you use the default
export, call its methods directly. Don't spread it, copy it with `Object.assign`, or list its
methods with `Object.keys`: on the New Architecture a method only becomes an own property of the
module object once it has been read, so those give an incomplete, timing-dependent set.

### Unchanged

* `Knot.open`, `Knot.openWithoutDelay`, `Knot.close`, `Knot.getSdkVersion` and `addKnotListener`.
* The event names (`knot:onSuccess`, `knot:onError`, `knot:onEvent`, `knot:onExit`) and the
  exported types (`KnotSuccess`, `KnotError`, `KnotEvent`, `KnotExit`).
* The error when the native module isn't linked. It is still thrown when the package is imported
  (the `pod install` line appears on iOS only):

  ```text theme={"system"}
  The package 'react-native-knotapi' doesn't seem to be linked. Make sure:

  - You have run 'pod install'
  - You rebuilt the app after installing the package
  - You are not using Expo managed workflow
  ```

  In React Native's SwiftPM mode, run `npx react-native spm` instead of `pod install`.

## Behaviour changes

This list is the changelog of what 3.0.0 changes at runtime, compared with 2.2.1 and 2.3.0.

### `open()` input

| Platform | 2.x | 3.0.0 |
| - | - | - |
| Android | `product: 'transactions_link'` (the spelling the `Config` type documents) crashed the app. | Accepted; mapped to the SDK's `transaction_link`. |
| Android | An unknown `product` crashed the app. | Ignored, as on iOS. The SDK takes the product from the session. |
| Both | An unknown `environment` crashed the app on Android. On iOS it fell back to `production`, so a session created for `development` got `knot:onError` with `errorCode: 'Invalid_Session'`, then `knot:onExit`. | `knot:onError` with `errorCode: 'INVALID_PARAMS'` (`"Invalid environment: <value>"`, or `"Missing required parameter (environment)"` when it is missing), then `knot:onExit`. |
| Both | A missing `clientId` crashed the app on Android. `Knot.open()` with no config, `null` or a non-object did nothing on iOS and crashed on Android. | `knot:onError` with `INVALID_PARAMS` (`"Missing required parameters (sessionId or clientId)"`), then `knot:onExit`. |
| Android | A non-string `metadata` value crashed the app. | Dropped, as on iOS. `metadata` is typed `Record<string, string>`. |
| Both | An empty `sessionId` gave `INVALID_PARAMS` with no `onExit` on iOS, and the SDK's `Invalid_Session` on Android. | `knot:onError` with `INVALID_PARAMS`, then `knot:onExit`, on both. |
| Android | A non-boolean `useCategories` / `useSearch` (`"true"`, `1`) made `open()` throw. | Ignored; the default applies. **On iOS:** it still treats `"true"` and non-zero numbers as `true`. Pass booleans. |

### Errors and `onExit`

| Platform | 2.x | 3.0.0 |
| - | - | - |
| iOS | `INVALID_PARAMS` was never followed by `knot:onExit`. | `INVALID_PARAMS` is followed by `knot:onExit`, except while a Knot flow is already open. |
| Android | `Activity_Not_Found` (`open()` with no current Activity) wasn't followed by `knot:onExit`. | Followed by `knot:onExit`, except while a Knot flow is already open. `Activity_Not_Found` is Android only. |
| Both | `Knot.close()` mid-flow sent no `knot:onExit` on iOS. On Android, a `close()` after the flow had ended sent a second `onExit`. | Exactly one `knot:onExit` per flow on both. `Knot.close()` mid-flow sends it; `Knot.close()` when no flow is open sends nothing. On Android the bridge sends a flow's `knot:onExit` itself where the SDK sends none: on `Knot.close()` when the SDK no longer holds the flow (an open that failed inside the SDK), and for an open the SDK dropped without a callback, once your Activity has stayed in the foreground for 5 seconds without Knot's screen appearing. |
| Both | iOS dropped events after `Knot.close()` until the next `Knot.open()`. Android dropped nothing: a flow's late `knot:onEvent`, `knot:onSuccess` or `knot:onError` reached JS after its `knot:onExit`. | A flow sends nothing after its `knot:onExit`, on both: its later `knot:onEvent`, `knot:onSuccess` and `knot:onError` are dropped. On iOS this now also holds after the flow's own `knot:onExit`, not only after `Knot.close()`. |
| Both | On iOS, an invalid `open()` while a flow was still on screen reset that flow's event `product`, so its later events lacked it. | An invalid `open()`, or on Android `Activity_Not_Found`, while a flow is open sends `knot:onError` only. The open flow, its `product` and its `knot:onExit` are left alone. |
| Both | A JS reload (dev reload or OTA restart) while Knot was open left the Knot UI up. On Android with the New Architecture, its events reached the new JS runtime. | The Knot UI is closed and nothing from that flow reaches the reloaded JS, including `knot:onExit`. See [Reloads and React hosts](#reloads-and-react-hosts). |
| Android | A valid `Knot.open()` while a flow was open replaced that flow, which never got its `knot:onExit`. | The replaced flow gets its `knot:onExit` just before the new flow opens. **On iOS:** the iOS SDK ignores the second open, while the bridge still clears the first flow's `product` and drops its callbacks already queued. Wait for `knot:onExit` before opening again. |

### Event payloads

| Platform | 2.x | 3.0.0 |
| - | - | - |
| iOS | `knot:onEvent` `metaData` flags were strings: `{ sendCard: "true" }`. `"false"` is truthy in JS. | Top-level `metaData` values that are exactly `"true"` or `"false"` are sent as booleans, as on Android: `{ sendCard: true }`. Check `metaData.sendCard === true`, not `=== "true"`. |
| iOS | An empty `metaData` was left out of the event. | `metaData` is always sent, as `{}` when empty, as on Android. |
| Android | An event whose payload had no `product` was lost. | Delivered, with the `product` of an earlier event of the same flow, or else the one passed to `open()`. |
| iOS | A blank `taskId` could be sent as `""`. | A blank `taskId` is left out, as on Android. A blank `merchantId` / `merchantName` is still sent as `""` on both. |
| Android | A `metaData` containing a nested object, a `null` or a 64-bit integer value was dropped as a whole, and arrays became JSON strings. | Passed through: nested objects and arrays kept, every number a JS number, `null` kept. **On iOS:** it still removes `null`s and empty nested objects and arrays. |
| iOS | `Knot.getSdkVersion()` returned `"Unknown"` when the native SDK reported no version. | Returns `undefined`, as on Android. The type is unchanged (`string \| undefined`). |

### Not changed in 3.0.0

* An invalid `clientId` is still `Invalid_Client_Id` on iOS and `Invalid_Session` on
  Android (the native SDKs differ).
* Events say `product: 'transaction_link'`, while `open()` accepts `'transactions_link'`.
* `domainUrls` is Android only, and iOS ignores `product` (the SDK takes it from the
  session).

## Reloads and React hosts

If the JS bundle reloads while the Knot UI is open (a dev reload, or an OTA restart such as
`expo-updates`' `reloadAsync`), the module closes the Knot UI, and that flow sends nothing more,
including `knot:onExit`, to the reloaded JS. Open a new session from the reloaded app if you need
one.

One React Native host per process is supported. With more than one, the Knot UI's events go to the
host whose `Knot.open` last opened it, and `Knot.close()` from any host closes it. The [React Native SDK](https://docs.knotapi.com/sdk/react-native.md#reloads-and-react-hosts) page has the details.

## Android hosts without `com.facebook.react`

On the New Architecture, an Android host must build React Native's `appmodules` library, which only
the React Native Gradle plugin (`com.facebook.react`) builds. This is React Native's requirement, not
one this package adds. Two setups work:

* the app applies the plugin, as every React Native CLI and Expo app does;
* a library module applies it and is packaged as an AAR for a native host, for example with
  Callstack's [React Native Brownfield](https://github.com/callstack/react-native-brownfield).

A hand-written Gradle host that applies the plugin nowhere builds, but fails at launch with
`'PlatformConstants' could not be found` (a React Native core module), before
`react-native-knotapi` is looked up.

For an AAR host:

* the AAR's metadata names `com.facebook.react:hermes-android:<React Native version>`, which isn't
  published. Substitute `com.facebook.hermes:hermes-android` at the `HERMES_VERSION_NAME` in
  `react-native/sdks/hermes-engine/version.properties`;
* the host's `compileSdk` must be at least the React Native template's (37 on React Native 0.87);
* the native Knot Android SDK (`com.knotapi.knot:knotapi-android-sdk`) reaches the host as a
  dependency of the AAR. A host that drops transitive dependencies must add it itself.

## Staying on 2.x

If you can't move to React Native 0.76 or iOS 15.1 yet, stay on 2.x:

```bash theme={"system"}
npm install react-native-knotapi@legacy-2x
```

Or pin the range in `package.json`:

```json theme={"system"}
{
  "dependencies": {
    "react-native-knotapi": "^2.3"
  }
}
```

From 3.0.0 on, `latest` points at 3.x and `legacy-2x` at the newest 2.x release. 2.x needs React
Native 0.73 or later and iOS 14.0 or later; see the
[2.3.0 README on npm](https://www.npmjs.com/package/react-native-knotapi/v/2.3.0).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.