# Insider ReactNative SDK

[![Coverus](https://github.com/useinsider/coverus-artifacts/raw/main/badges/github/useinsider/react-native-insider/coverage.svg)](http://coverus.internal.dataforce/app/project/github/useinsider/react-native-insider)
[![npm version](https://img.shields.io/npm/v/react-native-insider.svg)](https://www.npmjs.com/package/react-native-insider)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)

Official React Native SDK for the [Insider](https://www.insiderone.com) marketing
and growth platform. Wraps the native Insider SDK on Android and iOS and exposes
its features — user identification, event tracking, product and purchase events,
push notifications, smart recommendations, content optimizer, message center,
App Cards and App Frames — through a single `Insider` module with full TypeScript definitions.

## Installation

```bash
npm install react-native-insider
```

On iOS, install the pods afterwards:

```bash
cd ios && pod install
```

## Platform requirements

| Platform | Minimum |
|----------|---------|
| React Native | `>=0.74` (autolinking) |
| React | `>=18` |
| Android | `minSdk 24`, `compileSdk 36` |
| iOS | deployment target `12.0` |

App Frames on iOS additionally needs React Native `>=0.77` with the New
Architecture enabled; the rest of the SDK works on `>=0.74`.

The Insider pods are pinned by the podspec, so no version has to be declared
in your `Podfile`. On Android the SDK pulls in Firebase Messaging, Google Play
Services location and Huawei HMS push as transitive dependencies.

Gradle and Xcode setup — the Insider Maven repository, the `partner`
manifest placeholder, App Groups and the notification service extensions — is
covered in the integration guide and the sample app linked below.

## Quick start

```typescript
import Insider from 'react-native-insider';
import InsiderCallbackType from 'react-native-insider/src/InsiderCallbackType';

function onInsiderCallback(type, data) {
  switch (type) {
    case InsiderCallbackType.NOTIFICATION_OPEN:
      // Handle push tap.
      break;
    case InsiderCallbackType.INAPP_BUTTON_CLICK:
      // Handle in-app button tap.
      break;
    case InsiderCallbackType.INAPP_SEEN:
      // Handle in-app message displayed.
      break;
    case InsiderCallbackType.TEMP_STORE_PURCHASE:
      // Handle temp store purchase.
      break;
    case InsiderCallbackType.TEMP_STORE_ADDED_TO_CART:
      // Handle temp store add-to-cart.
      break;
    case InsiderCallbackType.TEMP_STORE_CUSTOM_ACTION:
      // Handle temp store custom action.
      break;
    case InsiderCallbackType.SESSION_STARTED:
      // Handle session start.
      break;
  }
}

Insider.init(
  'YOUR_PARTNER_NAME',
  'group.com.example.app', // iOS App Group; pass any value on Android.
  onInsiderCallback,
);

// Identify the user.
Insider.getCurrentUser()
  .setName('Ada')
  .setEmail('ada@example.com')
  .setEmailOptin(true);

// Tag a custom event with typed parameters.
Insider.tagEvent('add_to_favorites')
  .addParameterWithString('product_id', 'sku-123')
  .addParameterWithDouble('price', 29.99)
  .build();

// Attach objects to a user attribute, or to an event.
Insider.getCurrentUser()
  .addObjectsToCustomAttribute('cart_items', [{ sku: 'sku-123', qty: 2 }])
  .mergeObjectsInCustomAttribute('cart_items', { sku: 'sku-123' }, { qty: 3 })
  .removeObjectsFromCustomAttribute('cart_items', { sku: 'sku-123' });

Insider.tagEvent('order_placed')
  .addParameterWithObjectArray('order_lines', [{ sku: 'sku-123', qty: 2 }])
  .build();
```

### Attribute, parameter and object key names

Every key must match `^[a-z][0-9a-z_]*$` — a lowercase first letter, then lowercase letters, digits
and underscores. This is the native SDKs' own rule, and camelCase (`firstName`) fails it. A key that
fails is dropped with a `console.warn` naming it — native drops it too, but reports it only in its
own logs. Inside a `merge` or `remove` match a dropped key refuses the whole call, for the reason in
the table below.

### Values the SDK skips with a warning

Each is stopped before the bridge, with a `console.warn` naming what went and whether the rest of the
call was still sent. The last column says whether the row is only a new signal — native would have
dropped the value anyway — or a change in what reaches native.

| Value | What happens | Change |
|---|---|---|
| A product whose `price` is not a finite number | The product is left out. Multi-product calls still go out; a single-product call does not, and `getSmartRecommendationWithProduct` answers its callback with `{}` rather than never calling it. | behaviour |
| `...WithInt`, `setAge`, `setStock`, `setQuantity` outside int32 or fractional | The value is skipped. The native parameter is a 32-bit `int`, so the bridge cast would have stored a **different number**, not nothing. | behaviour |
| An invalid `Date` (`new Date('nope')`) | The value is skipped; its epoch would cross as the string `"NaN"`, which no native parser reads. | warning only |
| ...and that `Date` is a bound of `getMessageCenterData` or `getMessageCenterDataWithIdentifiers` | The call is not sent, and the callback is answered with an empty list rather than left uncalled — `[]` on iOS, `'[]'` on Android, the shape each platform's success path delivers. | behaviour |
| A blank identifier value (`addEmail('')`) | The identifier is skipped; native rejects a blank value silently anyway. | warning only |
| ...and that skip leaves the whole set empty | `login` answers its callback with `null` instead of never invoking it. | behaviour |
| A string over 500 characters in an object being **written** | The value is skipped, and with it its key. In a `merge`/`remove` **match** the refusal row applies instead. | warning only |
| ...and that leaves a `merge` **value** with no keys at all | The merge is not sent — it would have reached native writing nothing while reading as applied. | behaviour |
| ...or leaves `addObjectsToCustomAttribute` with no object at all | The call is not sent; native would have stored nothing for it either. | warning only |
| A `merge` or `remove` **match** that loses any key | The whole call is refused. A key is lost to the 500-character cap, a name that fails `^[a-z][0-9a-z_]*$`, an empty nested object, or nesting past one level — a narrowed match would select more objects than asked, and a remove cannot be undone. | behaviour |
| `merge` with an empty match or an empty value | The whole call is refused — an empty match would merge into **every** object. | behaviour |
| `customParameters` that is a class instance or other non-plain object | The call goes out without its parameters — as it already did, silently. | warning only |
| `customParameters` that is a **truthy** primitive or an array | The call goes out without its parameters — as it already did, silently. | warning only |
| Anything **truthy** other than a string or a plain object in the polymorphic second argument of `itemRemovedFromCart` / `visitCartPage` — a number, `true`, an **array**, a class instance | That argument is ignored; the rest of the call is still sent. | warning only |

Any falsy `customParameters` — `null`, `undefined`, `false`, `0`, `''` — is read as "not supplied" rather than
malformed, so those calls go out unchanged and raise no warning. The same holds in the polymorphic
second argument above — except `''`, which is a string and so crosses as an empty saleID.

### Removing objects

`removeObjectsFromCustomAttribute(key)` with no match removes **every** object of the attribute and
cannot be undone. Passing an empty match (`null`, `undefined` or `{}`) is refused instead, because
that shape is nearly always an unset variable rather than a deliberate remove-all.

TypeScript definitions for the full surface ship with the package
(`index.d.ts` and `src/*.d.ts`).

### Multiple apps under one partner

If your partner account serves several apps, pass the app identifier Insider
gave you as the optional last argument (`appIdentifier`), so sessions and push targeting are
attributed to the right app:

```typescript
Insider.init('YOUR_PARTNER_NAME', 'group.com.example.app', onInsiderCallback, 'my_app');

Insider.initWithCustomEndpoint(
  'YOUR_PARTNER_NAME',
  'group.com.example.app',
  'https://your.custom.endpoint',
  onInsiderCallback,
  'my_app',
);
```

- The identifier must match `^[a-z0-9][a-z0-9_]{0,49}$`. The native SDK
  validates it: an invalid value is logged and ignored, and initialization
  still completes.
- Omit it (or pass an empty string) to initialize without one; nothing about
  your integration changes. A non-string value logs a warning and is ignored.
- `reinitWithPartnerName` clears the app identifier on both platforms when it
  switches to a different partner name; the native SDK skips a reinit with the
  same name, so the identifier is kept.

## Documentation

- Integration guide: <https://academy.insiderone.com/docs/react-native-basic-sdk-setup>
- Changelog: <https://academy.insiderone.com/docs/react-native-changelogs>
- Sample app: <https://github.com/useinsider/ReactNativeDemo>
- Insider website: <https://insiderone.com/>
- Issue tracker: <https://github.com/useinsider/ReactNativeDemo/issues>

For panel-side setup — partner name, campaign and placement configuration —
contact your Insider account manager.

## License

react-native-insider is released under the MIT License. See [LICENSE](LICENSE).
