# Sirrus React Native SDK

Sirrus React Native SDK adds Sirrus event ingestion and Sirrus push notification handling to a React Native app.

It is designed to live alongside Firebase, MoEngage, Notifee, or an app's own notification code. The SDK handles only notifications that are explicitly marked for Martech handling.

## What The SDK Handles

- Initializes with a Sirrus SDK API key.
- Calls Sirrus SSO init internally and uses the returned org/project IDs, ingestion URL, notification URL, JWT, and PII public key.
- Creates and persists visitor, installation, and session IDs using storage provided by the host app.
- Tracks custom events and screen views through the Sirrus ingestion API.
- Registers the app's Firebase token as a `userToken` event after login.
- Receives and displays Sirrus push notifications.
- Supports rich push images and dynamic action buttons.
- Sends Sirrus push receive/open/action analytics to the `notificationUrl` returned by SSO init.

## What The Host App Owns

The SDK does not replace the app's Firebase/APNs setup.

The host app must:

- Configure Firebase and APNs.
- Add `google-services.json` / `GoogleService-Info.plist`.
- Request notification permission.
- Get the Firebase token.
- Pass the token to `registerToken` after the user is logged in.
- Keep existing handling for notifications that are not owned by this SDK.
- Add the required native extension/service setup for rich push where needed.

## Installation

```sh
yarn add sirrus-react-native-sdk
```

or:

```sh
npm install sirrus-react-native-sdk
```

For local tarball testing:

```sh
yarn add file:./sirrus-react-native-sdk-<version>.tgz
```

After installing, rebuild the native app. For iOS, run pods:

```sh
cd ios
pod install
```

## Quick Start

Initialize the SDK once when the app starts. Pass persistent storage from your app, such as MMKV or AsyncStorage.

```ts
import {
  initSirrusManagedSdk,
  sirrusReactNativeSDK,
} from "sirrus-react-native-sdk";

const sirrusStorage = {
  getItem: async (key: string) => mmkv.getString(key) ?? null,
  setItem: async (key: string, value: string) => {
    mmkv.set(key, value);
  },
  removeItem: async (key: string) => {
    mmkv.remove(key);
  },
};

await initSirrusManagedSdk(
  sirrusReactNativeSDK,
  {
    apiKey: "SIRRUS_SDK_API_KEY",
    storage: sirrusStorage,
    debug: __DEV__,
    appInfo: {
      name: "Your App",
      version: "1.0.0",
      buildNumber: "100",
      bundleId: "com.example.app",
      environment: "production",
    },
  },
  {
    firebaseMessaging: {
      onMessage: (listener) => messaging().onMessage(listener),
      onNotificationOpenedApp: (listener) =>
        messaging().onNotificationOpenedApp(listener),
      getInitialNotification: () => messaging().getInitialNotification(),
    },
  },
);
```

The default notification setup is usually enough:

- The SDK handles only Martech-marked notifications.
- Android uses a high-importance channel named `Sirrus Marketing` with ID `sirrus-marketing`.
- iOS foreground Sirrus notifications use the native iOS notification path.
- Android data-only Martech messages are rendered by the SDK native service when the SDK service is the active FCM receiver.

Use `notificationUI` only when your app needs custom Android channels, custom iOS categories, a custom presenter, or custom source matching.

For Android, product apps can pass channel IDs during init. The SDK creates those channels on the device, and incoming Martech notifications can target them with `data.android_channel_id`.

```ts
await initSirrusManagedSdk(sirrusReactNativeSDK, {
  apiKey: "SIRRUS_SDK_API_KEY",
  storage: sirrusStorage,
  notificationUI: {
    androidChannelIds: ["Promotion", "Updates"],
  },
});
```

## Register The User Token

Call `registerToken` after login, when your app has both the Firebase token and the logged-in user details.

`token`, `userId`, and `phone` are required. `userId` and `phone` are sent unencrypted because the backend needs them as plain root fields.

```ts
await sirrusReactNativeSDK.registerToken({
  token: fcmToken,
  userId: loggedInUserId,
  phone: loggedInPhoneNumber,
});
```

This sends an immediate ingestion request using the normal Sirrus batch envelope with a single `userToken` event.

The SDK also stores `userId` and `phone` in the provided storage and includes them in later event payloads.

You can update or clear user details explicitly:

```ts
await sirrusReactNativeSDK.setUserInfo({
  userId: loggedInUserId,
  phone: loggedInPhoneNumber,
});

await sirrusReactNativeSDK.setUserInfo(null);
```

## Track Events

```ts
await sirrusReactNativeSDK.track("booking_started", {
  entryPoint: "home",
});

await sirrusReactNativeSDK.screen("Home", {
  role: "customer",
});
```

Events are queued, persisted, batched, retried, and flushed through the ingestion URL returned by SSO init.

Each event includes device context and `context.device.sdkVersion` so the backend can identify which SDK version produced the payload.

## Optional PII Data

Use `setPii` only for extra PII values that should be encrypted before being sent to Sirrus.

```ts
sirrusReactNativeSDK.setPii({
  email: "user@example.com",
  name: "Example User",
});

sirrusReactNativeSDK.setPii(null);
```

The SDK encrypts this object with the PII public key returned by SSO init and sends it as the root `pii` field on ingestion batches.

If the React Native runtime does not provide WebCrypto, pass your own encryptor:

```ts
await sirrusReactNativeSDK.init({
  apiKey: "SIRRUS_SDK_API_KEY",
  storage: sirrusStorage,
  piiEncryptor: async (publicKey, payload) => {
    return encryptWithYourNativeCrypto(publicKey, payload);
  },
});
```

## Notification Ownership

The backend must mark SDK-owned notifications with `data.source` set to `martech`.

Notifications without the Martech marker are ignored by the SDK, so Firebase, MoEngage, Notifee, or your app's existing notification code can continue to handle them.

## Notification Analytics

SSO init returns two important URLs:

- `ingestionUrl` for normal analytics events and token registration.
- `notificationUrl` for Sirrus push lifecycle events.

The SDK sends notification analytics for:

- `received` when a Sirrus notification is received.
- `click` when the notification body is opened.
- `reply` when an action button is clicked.

For action buttons, the SDK includes the clicked action value and label when available. Campaign metadata is forwarded only when it is present in the notification data. The SDK does not invent campaign metadata.

All SDK API requests include these headers:

```txt
client_id: TCG-WEB-APP
application_platform: TCG-DXP-APP
```

## Android Setup

### If Your App Does Not Have A FirebaseMessagingService

The SDK includes `SirrusFirebaseMessagingService` in its Android library manifest. If your app does not declare another `FirebaseMessagingService`, Android can merge and use the SDK service automatically.

Your app still needs:

- Firebase configured in the app.
- `google-services.json` in the Android app.
- Notification permission requested by the app on Android 13+.
- A valid notification channel ID in the notification payload.

### If Your App Already Has A FirebaseMessagingService

Use one deterministic FCM receiver. In that receiver, let the SDK check the message first. If the SDK handles it, return immediately. Otherwise continue with your existing notification logic.

```kotlin
import ai.sirrus.reactnativesdk.SirrusPushMessagingHandler
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage

class AppFirebaseMessagingService : FirebaseMessagingService() {
  override fun onMessageReceived(remoteMessage: RemoteMessage) {
    if (SirrusPushMessagingHandler.handleRemoteMessage(applicationContext, remoteMessage)) {
      return
    }

    // Existing non-Sirrus handling, such as MoEngage, Notifee, or app logic.
  }

  override fun onNewToken(token: String) {
    SirrusPushMessagingHandler.handleNewToken(applicationContext, token, "fcm")

    // Existing product token handling.
  }
}
```

Declare your app service and remove the SDK auto service from the final merged manifest:

```xml
<manifest
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools">

  <application>
    <service
      android:name=".AppFirebaseMessagingService"
      android:exported="false">
      <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT" />
      </intent-filter>
    </service>

    <service
      android:name="ai.sirrus.reactnativesdk.SirrusFirebaseMessagingService"
      tools:node="remove" />
  </application>
</manifest>
```

### Android Payload Rules

For Android rich media and action buttons, the backend should send SDK-owned pushes as data-only FCM messages.

Do not include the top-level FCM `notification` object for Android SDK-owned pushes. Do not include `android.notification`. If Firebase renders the notification itself, the SDK cannot attach native action buttons.

Coordinate with the backend team to include `data.source = "martech"`, a unique message ID, title/body, optional raw image URL, optional action list, and a valid Android channel ID.

## Android Notification Channels

Android will drop or hide notifications that target a channel that does not exist on the device.

By default, the SDK creates:

```txt
channel id: sirrus-marketing
channel name: Sirrus Marketing
importance: high
```

If your backend sends a different `android_channel_id`, create that channel during SDK init.

For simple channels, pass only the IDs:

```ts
await sirrusReactNativeSDK.init({
  apiKey: "SIRRUS_SDK_API_KEY",
  storage: sirrusStorage,
  notificationUI: {
    androidChannelIds: ["Promotion", "Updates"],
  },
});
```

The `android_channel_id` value in the notification data must match the channel ID exactly:

```txt
android_channel_id: Promotion
```

If a user disables that channel in Android notification settings, Android will block notifications for that channel. The SDK does not override user notification settings.

For custom names or importance, pass full channel objects:

```ts
await sirrusReactNativeSDK.init({
  apiKey: "SIRRUS_SDK_API_KEY",
  storage: sirrusStorage,
  notificationUI: {
    defaultAndroidChannelId: "marketing",
    androidChannels: [
      {
        id: "marketing",
        name: "Marketing",
        importance: "high",
      },
    ],
  },
});
```

Use stable channel IDs. Android does not allow an app to change a channel's importance after the channel has been created on the device.

## iOS Setup

The main iOS app must have:

- Firebase/APNs configured.
- `GoogleService-Info.plist` added to the app target.
- Push Notifications capability enabled.
- Notification permission requested by the app.
- Sirrus initialized from React Native.
- Firebase token passed to `registerToken` after login.

### iOS Rich Push Extension

iOS requires a Notification Service Extension for rich media and dynamic action buttons in background or killed state.

Create one extension target per app bundle ID. For example:

```txt
Main app bundle ID: com.example.app
Extension bundle ID: com.example.app.SirrusNotificationService
Extension target: SirrusNotificationService
```

If your app has separate Dev, Staging, and Production bundle IDs, each app target needs its own embedded extension target:

```txt
com.example.app.dev.SirrusNotificationService
com.example.app.staging.SirrusNotificationService
com.example.app.SirrusNotificationService
```

### Recommended Extension Source Setup

Add the SDK notification-service source file to the extension target's Compile Sources:

```txt
node_modules/sirrus-react-native-sdk/ios/NotificationService/SirrusNotificationServiceExtension.swift
```

Then keep the extension's `NotificationService.swift` small:

```swift
import UserNotifications

final class NotificationService: SirrusNotificationServiceExtension {}
```

This method keeps the extension independent from React Native and avoids linking the full SDK framework into the extension.

When using this source-file method, do not also add a separate Podfile target for `sirrus-react-native-sdk/NotificationService`; the extension compiles the notification-service source directly.

The extension `Info.plist` must point to the service class:

```xml
<key>NSExtension</key>
<dict>
  <key>NSExtensionPointIdentifier</key>
  <string>com.apple.usernotifications.service</string>
  <key>NSExtensionPrincipalClass</key>
  <string>$(PRODUCT_MODULE_NAME).NotificationService</string>
</dict>
```

Make sure the extension target is embedded in the main app target under Embed App Extensions and is signed with the correct team/profile.

### iOS Notification Tap Forwarding

If your app owns `UNUserNotificationCenterDelegate`, forward notification taps to Sirrus before your other push handling. This lets Sirrus send click/action analytics even when a notification opens the app from killed state.

```swift
import SirrusReactNativeSdk
import UserNotifications

func application(
  _ application: UIApplication,
  didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
  UNUserNotificationCenter.current().delegate = self
  _ = SirrusNotificationResponseBridge.handleLaunchOptions(launchOptions)
  return true
}

func userNotificationCenter(
  _ center: UNUserNotificationCenter,
  didReceive response: UNNotificationResponse,
  withCompletionHandler completionHandler: @escaping () -> Void
) {
  if SirrusNotificationResponseBridge.handleNotificationResponse(response) {
    completionHandler()
    return
  }

  // Existing non-Sirrus notification handling.
  completionHandler()
}
```

### iOS Payload Rules

iOS rich notifications need:

- A visible APNs alert.
- `mutableContent: true`.
- Image URL in `apns.fcmOptions.imageUrl`.
- The same image URL in `data.imageUrl`.
- `data.actions` as a JSON string when action buttons are needed.

Use raw image URLs. Do not send Markdown link strings like `[https://...](https://...)`.

Coordinate with the backend team to include `data.source = "martech"`, a unique message ID, a visible APNs alert, `mutableContent: true`, optional raw image URL, and optional action list.

## Notification Interaction Listener

Use this when the product app wants to navigate or react after a Sirrus notification body/action click.

```ts
const unsubscribe = sirrusReactNativeSDK.onNotificationInteraction((event) => {
  const data = {
    ...(event.payload.rawPayload ?? {}),
    ...(event.payload.data ?? {}),
  };

  if (event.type === "opened") {
    // User tapped the notification body.
  }

  if (event.type === "action_pressed") {
    // User tapped an action button.
    console.log(event.actionId, event.action);
  }
});
```

If a notification opens the app before React Native subscribes, the SDK stores the interaction briefly and delivers it to the first listener.

## React Root Wrapper

`SirrusSdkRoot` is available as a compatibility wrapper. It does not render overlays.

```tsx
import { SirrusSdkRoot } from "sirrus-react-native-sdk";

export const App = () => {
  return (
    <SirrusSdkRoot>
      <Navigation />
    </SirrusSdkRoot>
  );
};
```

Using the wrapper is optional unless your app already relies on it.

## Setup Helper

The package includes a helper that can inspect common native setup issues.

```sh
npx sirrus-react-native-sdk setup
```

Use `--apply` only after reviewing what it will change:

```sh
npx sirrus-react-native-sdk setup --apply
```

Native projects differ a lot, especially when Firebase, MoEngage, Notifee, or multiple app targets are present. Always review generated native changes before committing.

## Useful APIs

```ts
sirrusReactNativeSDK.init(config);

sirrusReactNativeSDK.track(eventName, properties);
sirrusReactNativeSDK.screen(screenName, properties);

sirrusReactNativeSDK.registerToken({
  token,
  userId,
  phone,
});

sirrusReactNativeSDK.setUserInfo({ userId, phone });
sirrusReactNativeSDK.setUserInfo(null);

sirrusReactNativeSDK.setPii({ email, name });
sirrusReactNativeSDK.setPii(null);

sirrusReactNativeSDK.onNotificationInteraction((event) => {
  // Handle Sirrus notification body/action clicks.
});

sirrusReactNativeSDK.flush();
sirrusReactNativeSDK.shutdown();
```

## Troubleshooting

Foreground notification received but not displayed:

- Confirm the payload has `source: "martech"`.
- Confirm the app has notification permission.
- On Android, confirm the message is data-only.
- On iOS, confirm the notification delegate forwards Sirrus notifications or the SDK delegate is active.

Android image or action buttons missing:

- Do not send a top-level `notification` object for Android SDK-owned pushes.
- Do not send `android.notification`.
- Confirm `android_channel_id` exists on the device.
- Confirm `data.actions` is a JSON string.

iOS image or action buttons missing:

- Confirm the Notification Service Extension target is embedded in the app target you are running.
- Confirm the extension bundle ID is prefixed by the main app bundle ID.
- Confirm `mutableContent: true` is present.
- Confirm `apns.fcmOptions.imageUrl` and `data.imageUrl` are raw URLs.
- Use a direct `.jpg` or `.png` URL for the simplest rich-image test.

Other notifications affected:

- Confirm the SDK only receives messages with `source: "martech"`.
- If your app has its own `FirebaseMessagingService`, call `SirrusPushMessagingHandler.handleRemoteMessage(...)` first and continue existing logic only when it returns `false`.

## Release Checklist

Before publishing a new SDK version:

- Run `pnpm lint`.
- Run `pnpm build`.
- Run `pnpm test`.
- Run `git diff --check`.
- Test Android foreground, background, and killed-state Sirrus notifications.
- Test iOS foreground, background, and killed-state Sirrus notifications.
- Verify image rendering and action buttons.
- Verify notification receive/open/action analytics.
- Verify non-Sirrus notifications still work.
