Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
# [Unreleased]

## What's new

- New `setSilentMessageEventHandler()` — see [Set Silent Message Event Handler](https://github.com/emartech/react-native-emarsys-sdk/wiki/New-Architecture-and-Expo-framework#4-set-silent-message-event-handler)

# 2.0.0-beta.2

## What's new

- Expo plugin: support `androidGoogleServicesJsonPath` config option
- Expo plugin: support `androidSmallNotificationIconPath` config option
- Expo plugin: automatically inject `-Xskip-metadata-version-check` for Android Kotlin compatibility — see [Android Kotlin compatibility](https://github.com/emartech/react-native-emarsys-sdk/wiki/New-Architecture-and-Expo-framework#android-kotlin-compatibility
- Expo plugin: automatically inject `-Xskip-metadata-version-check` for Android Kotlin compatibility — see [Android Kotlin compatibility](https://github.com/emartech/react-native-emarsys-sdk/wiki/New-Architecture-and-Expo-framework#android-kotlin-compatibility)
- Expo plugin: support iOS Silent Push

## What's fixed
Expand Down
24 changes: 24 additions & 0 deletions __tests__/wrapper/NativeEmarsys.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ describe('NativeEmarsys', () => {
mockConfig = {
onEvent: jest.fn(),
setEventHandler: jest.fn(),
setSilentMessageEventHandler: jest.fn(),
setContact: jest.fn(),
clearContact: jest.fn(),
trackCustomEvent: jest.fn(),
Expand Down Expand Up @@ -54,6 +55,29 @@ describe('NativeEmarsys', () => {
});
});

describe('setSilentMessageEventHandler', () => {
it('should call setSilentMessageEventHandler', async () => {
(mockConfig.setSilentMessageEventHandler as jest.Mock).mockResolvedValue(undefined);

await mockConfig.setSilentMessageEventHandler();

expect(mockConfig.setSilentMessageEventHandler).toHaveBeenCalledTimes(1);
});

it('should handle successful setSilentMessageEventHandler', async () => {
(mockConfig.setSilentMessageEventHandler as jest.Mock).mockResolvedValue(undefined);

await expect(mockConfig.setSilentMessageEventHandler()).resolves.toBeUndefined();
});

it('should handle errors when setSilentMessageEventHandler', async () => {
const error = new Error('Failed to setSilentMessageEventHandler');
(mockConfig.setSilentMessageEventHandler as jest.Mock).mockRejectedValue(error);

await expect(mockConfig.setSilentMessageEventHandler()).rejects.toThrow('Failed to setSilentMessageEventHandler');
});
});

describe('setContact', () => {
it('should call setContact with parameter', async () => {
const contactFieldId = 123456;
Expand Down
7 changes: 6 additions & 1 deletion android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
<manifest>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<service
android:name="com.emarsys.reactnative.RNEmarsysHeadlessJsTaskService"
android:exported="false" />
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.emarsys.reactnative

import android.content.Context
import android.content.Intent
import com.emarsys.reactnative.utils.MapUtils.toWritableMap
import com.facebook.react.HeadlessJsTaskService
import com.facebook.react.bridge.Arguments
import com.facebook.react.jstasks.HeadlessJsTaskConfig
import org.json.JSONObject

class RNEmarsysHeadlessJsTaskService : HeadlessJsTaskService() {

companion object {
const val TASK_NAME = "EmarsysSilentMessageEvent"
const val EXTRA_EVENT_NAME = "eventName"
const val EXTRA_PAYLOAD = "payload"

fun start(context: Context, intent: Intent) {
acquireWakeLockNow(context)
context.startService(intent)
}
Comment thread
biancalui-emarsys marked this conversation as resolved.
}

override fun getTaskConfig(intent: Intent?): HeadlessJsTaskConfig? {
val eventName = intent?.getStringExtra(EXTRA_EVENT_NAME) ?: return null
val payload = intent.getStringExtra(EXTRA_PAYLOAD)?.let { it -> JSONObject(it) }
val data = JSONObject().put("name", eventName).put("payload", payload).toWritableMap()
Comment thread
biancalui-emarsys marked this conversation as resolved.
return HeadlessJsTaskConfig(TASK_NAME, data, 30_000, true)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ package com.emarsys.reactnative.expo

import android.app.Application
import com.emarsys.Emarsys
import com.emarsys.reactnative.utils.EventUtils
import com.emarsys.reactnative.RNEmarsys
import com.emarsys.reactnative.utils.StorageUtils
import expo.modules.core.interfaces.ApplicationLifecycleListener

Expand All @@ -15,8 +15,7 @@ class EmarsysApplicationLifecycleListener : ApplicationLifecycleListener {

val config = StorageUtils.getEmarsysConfig(application)
Emarsys.setup(config)

EventUtils.setEventHandler()
RNEmarsys.setup()
}

}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.emarsys.reactnative.utils

import android.content.Context
import android.content.Intent
import com.emarsys.Emarsys
import com.emarsys.mobileengage.api.event.EventHandler
import com.emarsys.reactnative.RNEmarsysHeadlessJsTaskService
import org.json.JSONObject

object EventUtils {
Expand All @@ -17,7 +19,6 @@ object EventUtils {
eventsCache.add(mapOf("context" to context, "eventName" to eventName, "payload" to payload))
}
Emarsys.push.setNotificationEventHandler(_eventHandler)
Emarsys.push.setSilentMessageEventHandler(_eventHandler)
Emarsys.inApp.setEventHandler(_eventHandler)
Emarsys.geofence.setEventHandler(_eventHandler)
Emarsys.onEventAction.setOnEventActionEventHandler(_eventHandler)
Expand All @@ -30,4 +31,14 @@ object EventUtils {
}
}

fun setSilentMessageEventHandler() {
Emarsys.push.setSilentMessageEventHandler { context: Context, eventName: String, payload: JSONObject? ->
val intent = Intent(context, RNEmarsysHeadlessJsTaskService::class.java).apply {
putExtra(RNEmarsysHeadlessJsTaskService.EXTRA_EVENT_NAME, eventName)
putExtra(RNEmarsysHeadlessJsTaskService.EXTRA_PAYLOAD, payload?.toString())
}
RNEmarsysHeadlessJsTaskService.start(context, intent)
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ class NativeEmarsys(reactContext: ReactApplicationContext) : NativeEmarsysSpec(r
promise.resolve(null)
}

override fun setSilentMessageEventHandler(promise: Promise) {
EventUtils.setSilentMessageEventHandler()
promise.resolve(null)
}

override fun setContact(contactFieldId: Double, contactFieldValue: String, promise: Promise) {
try {
Emarsys.setContact(contactFieldId.toInt(), contactFieldValue) { errorCause: Throwable? ->
Expand Down
6 changes: 3 additions & 3 deletions docs/Migration-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ import Emarsys from 'react-native-emarsys-wrapper';

**New:**
```javascript
import Emarsys from 'react-native-emarsys-sdk';
import Emarsys from '@emartech/react-native-emarsys-sdk';
```

### Contact Management
Expand Down Expand Up @@ -235,7 +235,7 @@ this.inlineInAppView.current.loadInApp('view-id')

**New:**
```javascript
import { InlineInAppView } from 'react-native-emarsys-sdk';
import { InlineInAppView } from '@emartech/react-native-emarsys-sdk';

const inlineInAppView = useRef<any>(null);
const [inlineInAppViewHeight, setInlineInAppViewHeight] = useState(0);
Expand Down Expand Up @@ -433,7 +433,7 @@ await Emarsys.geofence.getRegisteredGeofences();
- Event structure: `(eventName, payload)` → `(event: Event)` with `event.name` and `event.payload`

### Inline InApp
- Import separately: `import { InlineInAppView } from 'react-native-emarsys-sdk'`
- Import separately: `import { InlineInAppView } from '@emartech/react-native-emarsys-sdk'`
- Loading: `ref.current.loadInApp(viewId)` → `Emarsys.inApp.loadInlineInApp(ref.current, viewId)`

### Predict API
Expand Down
27 changes: 19 additions & 8 deletions docs/New-Architecture-and-Expo-framework.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
- [1. Set Push Token](#1-set-push-token)
- [2. Clear Push Token](#2-clear-push-token)
- [3. Get Push Token](#3-get-push-token)
- [4. Set Silent Message Event Handler](#4-set-silent-message-event-handler)
- [InApp](#inapp)
- [1. Overlay InApp](#1-overlay-inapp)
- [1.1. Pause](#11-pause)
Expand Down Expand Up @@ -480,7 +481,7 @@ From Android 12, when the `ACCESS_FINE_LOCATION` permission is granted to the Ap

## Import
```javascript
import Emarsys from 'react-native-emarsys-sdk';
import Emarsys from '@emartech/react-native-emarsys-sdk';
```

## Contact Management
Expand Down Expand Up @@ -524,7 +525,7 @@ Add `setEventHandler` to the `useEffect` in your `App.js`.
`setEventHandler` returns React Native `EventSubscription`, which could be unsubscribed (`remove()`) when needed, e.g. in the cleanup function.

```javascript
import { type Event } from 'react-native-emarsys-sdk';
import { type Event } from '@emartech/react-native-emarsys-sdk';

const eventHandlerSubscription = useRef<EventSubscription | null>(null);

Expand Down Expand Up @@ -575,6 +576,16 @@ If you want to get the push token for the contact, please use `getPushToken()` m
const pushToken = await Emarsys.push.getPushToken();
```

### 4. Set Silent Message Event Handler

To handle silent push notifications, register a handler at module level in your `index.js` — outside any React component, before `AppRegistry.registerComponent`.

```javascript
Emarsys.setSilentMessageEventHandler(async (event) => {
console.log('Silent Message Event', event.name, JSON.stringify(event.payload));
});
```

## InApp

### 1. Overlay InApp
Expand All @@ -601,7 +612,7 @@ In-App message, that takes place in the application's view hierarchy. Multiple i

Create the view with component `InlineInAppView`.
```javascript
import { InlineInAppView } from 'react-native-emarsys-sdk';
import { InlineInAppView } from '@emartech/react-native-emarsys-sdk';

const inlineInAppView = useRef<any>(null);
const [inlineInAppViewHeight, setInlineInAppViewHeight] = useState(0);
Expand Down Expand Up @@ -646,7 +657,7 @@ To use the Predict functionality you have to setup your `merchantId` during the
When you want to track the cart items in the basket, you can call the `trackCart()` method with a list of `CartItem`. `CartItem` is an interface that can be used in your application for your own `CartItem` and then simply use the same items with the SDK.

```javascript
import { type CartItem } from 'react-native-emarsys-sdk';
import { type CartItem } from '@emartech/react-native-emarsys-sdk';

const items: CartItem[] = [
{ itemId: 'item1', price: 1.1, quantity: 1 },
Expand Down Expand Up @@ -725,7 +736,7 @@ await Emarsys.predict.trackTag(tag, attributes);
With the Emarsys SDK you can ask for product recommendations based on different recommendation parameters.

```javascript
import { Logic, Filter, type Product } from 'react-native-emarsys-sdk';
import { Logic, Filter, type Product } from '@emartech/react-native-emarsys-sdk';

const logic = Logic.home(['1', '2']);
const filters = [Filter.exclude.isValue('field', 'value')];
Expand Down Expand Up @@ -913,7 +924,7 @@ const RNWrapperVersion = await Emarsys.config.getRNWrapperVersion();
In order to receive the message Inbox content, you can use the `fetchMessages()` method.

```javascript
import { type Message } from 'react-native-emarsys-sdk';
import { type Message } from '@emartech/react-native-emarsys-sdk';

const inboxMessages: Message[] = await Emarsys.inbox.fetchMessages();
```
Expand All @@ -935,7 +946,7 @@ Tags are to be used to set the status of the inbox message, e.g. opened, seen et

#### 2.1 Tags
```javascript
import { Tag } from 'react-native-emarsys-sdk';
import { Tag } from '@emartech/react-native-emarsys-sdk';
```

#### 2.2 Add Tag
Expand Down Expand Up @@ -1028,7 +1039,7 @@ await Emarsys.geofence.setInitialEnterTriggerEnabled(true);
You can access the registered geofences from the device using the `getRegisteredGeofences()` method.

```javascript
import { type Geofence } from 'react-native-emarsys-sdk';
import { type Geofence } from '@emartech/react-native-emarsys-sdk';

const geofences: Geofence[] = await Emarsys.geofence.getRegisteredGeofences();
```
8 changes: 7 additions & 1 deletion example/expo/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { registerRootComponent } from 'expo';

import Emarsys from '@emartech/react-native-emarsys-sdk';
import App from './App';

// Register silent message event handler when the app is killed.
// Must be at module level — outside any React component.
Emarsys.setSilentMessageEventHandler(async (event) => {
console.log('Silent Message Event', '-', `${event.name}: ${JSON.stringify(event.payload)}`);
});

// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
// It also ensures that whether you load the app in Expo Go or in a native build,
// the environment is set up appropriately
Expand Down
11 changes: 7 additions & 4 deletions example/react-native/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
/**
* @format
*/

import { AppRegistry } from 'react-native';
import Emarsys from '@emartech/react-native-emarsys-sdk';
import App from './App';
import { name as appName } from './app.json';

// Register silent message event handler when the app is killed.
// Must be at module level — outside any React component.
Emarsys.setSilentMessageEventHandler(async (event) => {
console.log('Silent Message Event', '-', `${event.name}: ${JSON.stringify(event.payload)}`);
});

AppRegistry.registerComponent(appName, () => App);
3 changes: 1 addition & 2 deletions ios/expo/EmarsysAppDelegateSubscriber.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@ public class EmarsysAppDelegateSubscriber: ExpoAppDelegateSubscriber {
public func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
let config = StorageUtils.getEMSConfig()!
Emarsys.setup(config: config)

UNUserNotificationCenter.current().delegate = Emarsys.push
EventUtils.setEventHandler(nil)
RNEmarsys.setup()

return true
}
Expand Down
1 change: 1 addition & 0 deletions ios/utils/EventUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
@interface EventUtils : NSObject

+ (void)setEventHandler:(EMSEventHandlerBlock)eventHandler;
+ (void)setSilentMessageEventHandler:(EMSEventHandlerBlock)eventHandler;

@end
7 changes: 5 additions & 2 deletions ios/utils/EventUtils.m
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,10 @@ + (void)setEventHandler:(EMSEventHandlerBlock)eventHandler {
[eventsCache addObject:@{@"eventName": eventName, @"payload": payload}];
};
Emarsys.push.notificationEventHandler = _eventHandler;
Emarsys.push.silentMessageEventHandler = _eventHandler;
Emarsys.inApp.eventHandler = _eventHandler;
Emarsys.geofence.eventHandler = _eventHandler;
Emarsys.onEventAction.eventHandler = _eventHandler;

if (eventHandler != nil && eventsCache != nil) {
for (NSDictionary *event in eventsCache) {
eventHandler([event objectForKey:@"eventName"], [event objectForKey:@"payload"]);
Expand All @@ -25,4 +24,8 @@ + (void)setEventHandler:(EMSEventHandlerBlock)eventHandler {
}
}

+ (void)setSilentMessageEventHandler:(EMSEventHandlerBlock)eventHandler {
Emarsys.push.silentMessageEventHandler = eventHandler;
}

@end
9 changes: 8 additions & 1 deletion ios/wrapper/NativeEmarsys.mm
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,14 @@ @implementation NativeEmarsys

- (void)setEventHandler:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
[EventUtils setEventHandler:^(NSString *eventName, NSDictionary<NSString *, id> *payload) {
[self emitOnEvent:@{@"name": eventName, @"payload": payload}];
[self emitOnEvent:@{@"name": eventName, @"payload": payload ?: [NSNull null]}];
}];
resolve(nil);
}

- (void)setSilentMessageEventHandler:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
[EventUtils setSilentMessageEventHandler:^(NSString *eventName, NSDictionary<NSString *, id> *payload) {
[self emitOnSilentMessageEvent:@{@"name": eventName, @"payload": payload ?: [NSNull null]}];
}];
resolve(nil);
}
Expand Down
2 changes: 2 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export const STORE_NAME = 'com.emarsys.reactnative';

export const SILENT_MESSAGE_EVENT_HEADLESS_TASK_NAME = 'EmarsysSilentMessageEvent';
Loading
Loading