cordova-plugin-reteno 2.0.4 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/ios.md CHANGED
@@ -68,17 +68,16 @@ target 'App' do
68
68
 
69
69
  target 'NotificationServiceExtension' do
70
70
  inherit! :search_paths
71
- pod 'Reteno', '2.7.0'
71
+ pod 'Reteno', '2.7.3'
72
72
  end
73
73
 
74
74
  target 'NotificationContentExtension' do
75
75
  inherit! :search_paths
76
- pod 'Reteno', '2.7.0'
76
+ pod 'Reteno', '2.7.3'
77
77
  end
78
78
  end
79
79
  ```
80
80
 
81
-
82
81
  ## Add Notification Content Extension (NCE) — Images Carousel
83
82
 
84
83
  The Notification Content Extension is required for rich push notifications with an **Images Carousel**.
@@ -86,7 +85,7 @@ It provides the `UNNotificationContentExtension` entry point that renders a cust
86
85
 
87
86
  Official guide: https://docs.reteno.com/reference/ios-images-carousel
88
87
 
89
- ### Create the extension target *(manual Xcode step)*
88
+ ### Create the extension target _(manual Xcode step)_
90
89
 
91
90
  In Xcode: **File → New → Target → Notification Content Extension**. Name it `NotificationContentExtension`.
92
91
 
@@ -130,7 +129,7 @@ Open `NotificationContentExtension/Info.plist` as source code and replace the `N
130
129
 
131
130
  Make sure the NCE **Deployment Target** in Xcode matches the main app target (iOS 15.0+). A mismatch is the most common reason carousel images fail to display.
132
131
 
133
- ### Embed App Extensions — Copy only when installing *(manual Xcode step)*
132
+ ### Embed App Extensions — Copy only when installing _(manual Xcode step)_
134
133
 
135
134
  > **This step must be done manually in Xcode** — it cannot be automated.
136
135
  > Official guide (Troubleshooting → "Turn off Copy Only when Installing"): https://docs.reteno.com/reference/ios-images-carousel
@@ -187,12 +186,12 @@ target 'App' do
187
186
 
188
187
  target 'NotificationServiceExtension' do
189
188
  inherit! :search_paths
190
- pod 'Reteno', '2.7.0'
189
+ pod 'Reteno', '2.7.3'
191
190
  end
192
191
 
193
192
  target 'NotificationContentExtension' do
194
193
  inherit! :search_paths
195
- pod 'Reteno', '2.7.0'
194
+ pod 'Reteno', '2.7.3'
196
195
  end
197
196
  end
198
197
  ```
@@ -307,6 +306,7 @@ If your app calls `FirebaseApp.configure()` without this file, Firebase will cra
307
306
  Nothing to do. Firebase iOS SDK uses **method swizzling** by default — it automatically intercepts `didRegisterForRemoteNotificationsWithDeviceToken` and forwards the APNS token to Firebase without any AppDelegate code.
308
307
 
309
308
  When `IOS_DEVICE_TOKEN_HANDLING_MODE` is set to `manual`, the plugin:
309
+
310
310
  1. Calls `FirebaseApp.configure()` automatically on `init()` if not already called and `GoogleService-Info.plist` is present.
311
311
  2. Subscribes to FCM token updates and forwards them to Reteno once Firebase is configured.
312
312
 
@@ -381,7 +381,7 @@ await RetenoPlugin.init({
381
381
  });
382
382
 
383
383
  // Runtime pause/resume
384
- await RetenoPlugin.pauseInAppMessages(true); // pause
384
+ await RetenoPlugin.pauseInAppMessages(true); // pause
385
385
  await RetenoPlugin.pauseInAppMessages(false); // resume
386
386
  ```
387
387
 
@@ -403,7 +403,7 @@ Set presentation options for foreground notifications:
403
403
  ```js
404
404
  await RetenoPlugin.setWillPresentNotificationOptions({
405
405
  options: ['badge', 'sound', 'banner'],
406
- emitEvent: true
406
+ emitEvent: true,
407
407
  });
408
408
  ```
409
409
 
@@ -414,7 +414,7 @@ Enable a response handler for notification taps:
414
414
  ```js
415
415
  await RetenoPlugin.setDidReceiveNotificationResponseHandler({
416
416
  enabled: true,
417
- emitEvent: true
417
+ emitEvent: true,
418
418
  });
419
419
  ```
420
420
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cordova-plugin-reteno",
3
- "version": "2.0.4",
3
+ "version": "2.2.0",
4
4
  "description": "The Reteno Cordova Plugin for Mobile Customer Engagement and Analytics solutions",
5
5
  "types": "./types/index.ts",
6
6
  "files": [
package/plugin.xml CHANGED
@@ -1,5 +1,5 @@
1
1
  <?xml version='1.0' encoding='utf-8'?>
2
- <plugin id="cordova-plugin-reteno" version="2.0.4" xmlns="http://apache.org/cordova/ns/plugins/1.0" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
2
+ <plugin id="cordova-plugin-reteno" version="2.2.0" xmlns="http://apache.org/cordova/ns/plugins/1.0" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
3
3
  <name>cordova-plugin-reteno</name>
4
4
  <engines>
5
5
  <engine name="cordova-android" version=">=12.0.0" />
@@ -22,6 +22,10 @@
22
22
 
23
23
  <config-file target="AndroidManifest.xml" parent="/manifest/application">
24
24
  <meta-data android:name="com.reteno.SDK_ACCESS_KEY" android:value="$SDK_ACCESS_KEY" />
25
+ <provider
26
+ android:name="com.reteno.plugin.RetenoNotificationGroupingRuleProvider"
27
+ android:authorities="${applicationId}.reteno.notification-grouping-rule"
28
+ android:exported="false" />
25
29
  </config-file>
26
30
 
27
31
  <config-file target="config.xml" parent="/*">
@@ -45,6 +49,12 @@
45
49
  <source-file
46
50
  src="src/android/app/src/main/java/com/reteno/plugin/RetenoUtil.java"
47
51
  target-dir="src/com/reteno/plugin" />
52
+ <source-file
53
+ src="src/android/app/src/main/java/com/reteno/plugin/RetenoNotificationGroupingRuleProvider.java"
54
+ target-dir="src/com/reteno/plugin" />
55
+ <source-file
56
+ src="src/android/app/src/main/java/com/reteno/plugin/RetenoNotificationSummaryManager.java"
57
+ target-dir="src/com/reteno/plugin" />
48
58
  </platform>
49
59
 
50
60
  <platform name="ios">
@@ -58,7 +68,7 @@
58
68
 
59
69
  <podspec>
60
70
  <pods>
61
- <pod name="Reteno" spec="2.7.0" />
71
+ <pod name="Reteno" spec="2.7.3" />
62
72
  <pod name="FirebaseMessaging" />
63
73
  </pods>
64
74
  </podspec>
@@ -1,6 +1,6 @@
1
1
  apply plugin: 'com.android.application'
2
2
 
3
- def retenoFcmVersion = '2.9.4'
3
+ def retenoFcmVersion = '2.10.1'
4
4
 
5
5
  android {
6
6
  namespace 'com.reteno.plugin'
@@ -2,6 +2,11 @@
2
2
  <manifest xmlns:android="http://schemas.android.com/apk/res/android"
3
3
  package="com.reteno.plugin">
4
4
 
5
- <application />
5
+ <application>
6
+ <provider
7
+ android:name="com.reteno.plugin.RetenoNotificationGroupingRuleProvider"
8
+ android:authorities="${applicationId}.reteno.notification-grouping-rule"
9
+ android:exported="false" />
10
+ </application>
6
11
 
7
12
  </manifest>
@@ -0,0 +1,117 @@
1
+ package com.reteno.plugin;
2
+
3
+ import android.content.ContentProvider;
4
+ import android.content.ContentValues;
5
+ import android.content.Context;
6
+ import android.content.SharedPreferences;
7
+ import android.database.Cursor;
8
+ import android.net.Uri;
9
+ import android.text.TextUtils;
10
+
11
+ import com.reteno.push.RetenoNotifications;
12
+
13
+ import java.util.Map;
14
+
15
+ /**
16
+ * Restores the declarative notification grouping rule before services handle a push.
17
+ * This keeps grouping available when the Cordova WebView has not started yet.
18
+ */
19
+ public final class RetenoNotificationGroupingRuleProvider extends ContentProvider {
20
+ private static final String PREFERENCES = "com.reteno.plugin.notification-grouping-rule";
21
+ private static final String PAYLOAD_KEY = "payloadKey";
22
+ private static final String GROUP_ID = "groupId";
23
+ private static final String SHOW_SUMMARY = "showSummary";
24
+
25
+ @Override
26
+ public boolean onCreate() {
27
+ Context context = getContext();
28
+ if (context != null) {
29
+ restore(context);
30
+ }
31
+ return true;
32
+ }
33
+
34
+ static void configure(Context context, String payloadKey, String groupId, boolean showSummary) {
35
+ SharedPreferences.Editor editor = preferences(context).edit().clear();
36
+ if (!TextUtils.isEmpty(payloadKey)) {
37
+ editor.putString(PAYLOAD_KEY, payloadKey);
38
+ } else if (!TextUtils.isEmpty(groupId)) {
39
+ editor.putString(GROUP_ID, groupId);
40
+ }
41
+ if (showSummary) {
42
+ editor.putBoolean(SHOW_SUMMARY, true);
43
+ }
44
+ // Persist before returning so a process restart cannot lose a just-configured rule.
45
+ editor.commit();
46
+ install(context, payloadKey, groupId, showSummary);
47
+ }
48
+
49
+ private static void restore(Context context) {
50
+ SharedPreferences preferences = preferences(context);
51
+ install(
52
+ context,
53
+ preferences.getString(PAYLOAD_KEY, null),
54
+ preferences.getString(GROUP_ID, null),
55
+ preferences.getBoolean(SHOW_SUMMARY, false)
56
+ );
57
+ }
58
+
59
+ /** Resolves the group a received push belongs to under the persisted rule. */
60
+ static String resolveGroup(Context context, Map<String, String> payload) {
61
+ SharedPreferences preferences = preferences(context);
62
+ String payloadKey = preferences.getString(PAYLOAD_KEY, null);
63
+ if (!TextUtils.isEmpty(payloadKey)) {
64
+ String value = payload.get(payloadKey);
65
+ return TextUtils.isEmpty(value) ? null : value;
66
+ }
67
+ String groupId = preferences.getString(GROUP_ID, null);
68
+ return TextUtils.isEmpty(groupId) ? null : groupId;
69
+ }
70
+
71
+ private static SharedPreferences preferences(Context context) {
72
+ return context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE);
73
+ }
74
+
75
+ private static void install(Context context, String payloadKey, String groupId, boolean showSummary) {
76
+ if (!TextUtils.isEmpty(payloadKey)) {
77
+ RetenoNotifications.setGroupingRule((Map<String, String> payload) -> {
78
+ String value = payload.get(payloadKey);
79
+ return TextUtils.isEmpty(value) ? null : value;
80
+ });
81
+ } else if (!TextUtils.isEmpty(groupId)) {
82
+ RetenoNotifications.setGroupingRule((Map<String, String> payload) -> groupId);
83
+ } else {
84
+ RetenoNotifications.setGroupingRule(null);
85
+ }
86
+ RetenoNotificationSummaryManager.setEnabled(
87
+ context,
88
+ showSummary && (!TextUtils.isEmpty(payloadKey) || !TextUtils.isEmpty(groupId)),
89
+ !TextUtils.isEmpty(payloadKey) ? "payloadKey:" + payloadKey : "groupId:" + groupId
90
+ );
91
+ }
92
+
93
+ @Override
94
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
95
+ return null;
96
+ }
97
+
98
+ @Override
99
+ public String getType(Uri uri) {
100
+ return null;
101
+ }
102
+
103
+ @Override
104
+ public Uri insert(Uri uri, ContentValues values) {
105
+ return null;
106
+ }
107
+
108
+ @Override
109
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
110
+ return 0;
111
+ }
112
+
113
+ @Override
114
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
115
+ return 0;
116
+ }
117
+ }
@@ -0,0 +1,262 @@
1
+ package com.reteno.plugin;
2
+
3
+ import android.Manifest;
4
+ import android.app.Notification;
5
+ import android.app.NotificationChannel;
6
+ import android.app.NotificationManager;
7
+ import android.content.Context;
8
+ import android.content.pm.PackageManager;
9
+ import android.os.Bundle;
10
+ import android.os.Handler;
11
+ import android.os.Looper;
12
+ import android.service.notification.StatusBarNotification;
13
+
14
+ import androidx.core.app.ActivityCompat;
15
+ import androidx.core.app.NotificationCompat;
16
+ import androidx.core.app.NotificationManagerCompat;
17
+
18
+ import com.reteno.core.util.Procedure;
19
+ import com.reteno.push.RetenoNotifications;
20
+
21
+ import java.util.HashMap;
22
+ import java.util.Map;
23
+ import java.util.Objects;
24
+
25
+ /** Maintains optional Android group-summary notifications for Reteno pushes. */
26
+ final class RetenoNotificationSummaryManager {
27
+ private static final String SUMMARY_CHANNEL_ID = "reteno_group_summary";
28
+ private static final String SUMMARY_TAG_PREFIX = "com.reteno.plugin.group-summary:";
29
+ private static final int SUMMARY_NOTIFICATION_ID = 1;
30
+ private static final int MAX_POST_ATTEMPTS = 25;
31
+ private static final int MAX_REMOVAL_ATTEMPTS = 10;
32
+ private static final long RETRY_DELAY_MS = 200L;
33
+
34
+ private static RetenoNotificationSummaryManager instance;
35
+
36
+ private final Context context;
37
+ private final Handler handler = new Handler(Looper.getMainLooper());
38
+ private final NotificationManagerCompat notificationManager;
39
+
40
+ private Procedure<Bundle> receivedListener;
41
+ private Procedure<Bundle> clickListener;
42
+ private Procedure<Bundle> closeListener;
43
+ private boolean enabled;
44
+ private String ruleIdentity;
45
+
46
+ private RetenoNotificationSummaryManager(Context context) {
47
+ this.context = context.getApplicationContext();
48
+ this.notificationManager = NotificationManagerCompat.from(this.context);
49
+ }
50
+
51
+ static synchronized void setEnabled(Context context, boolean enabled, String ruleIdentity) {
52
+ if (instance == null) {
53
+ instance = new RetenoNotificationSummaryManager(context);
54
+ }
55
+ if (enabled) {
56
+ instance.enable(ruleIdentity);
57
+ } else {
58
+ instance.disable();
59
+ }
60
+ }
61
+
62
+ private void enable(String ruleIdentity) {
63
+ if (enabled) {
64
+ if (Objects.equals(this.ruleIdentity, ruleIdentity)) {
65
+ return;
66
+ }
67
+ // The grouping rule may have changed since we were last enabled (e.g. constant group
68
+ // "A" -> "B"). Group "A" will never receive another received/click/close event under
69
+ // the new rule, so its summary would otherwise be orphaned forever. Drop any pending
70
+ // retry first - it closed over the old group and would otherwise recreate a stale
71
+ // summary via reconcile() right after we cancel it below - then cancel every summary
72
+ // this manager owns; each will be rebuilt from scratch as traffic for the new rule
73
+ // comes in. cancelAllSummaries() only touches our own SUMMARY_TAG_PREFIX-tagged
74
+ // notifications, so it never disturbs a group some other feature of the host app uses.
75
+ handler.removeCallbacksAndMessages(null);
76
+ cancelAllSummaries();
77
+ this.ruleIdentity = ruleIdentity;
78
+ return;
79
+ }
80
+ enabled = true;
81
+ this.ruleIdentity = ruleIdentity;
82
+ createSummaryChannel();
83
+
84
+ receivedListener = this::onReceived;
85
+ clickListener = this::onRemoved;
86
+ closeListener = this::onRemoved;
87
+ RetenoNotifications.getReceived().addListener(receivedListener);
88
+ RetenoNotifications.getClick().addListener(clickListener);
89
+ RetenoNotifications.getClose().addListener(closeListener);
90
+ }
91
+
92
+ private void disable() {
93
+ enabled = false;
94
+ ruleIdentity = null;
95
+ handler.removeCallbacksAndMessages(null);
96
+ if (receivedListener != null) {
97
+ RetenoNotifications.getReceived().removeListener(receivedListener);
98
+ receivedListener = null;
99
+ }
100
+ if (clickListener != null) {
101
+ RetenoNotifications.getClick().removeListener(clickListener);
102
+ clickListener = null;
103
+ }
104
+ if (closeListener != null) {
105
+ RetenoNotifications.getClose().removeListener(closeListener);
106
+ closeListener = null;
107
+ }
108
+ cancelAllSummaries();
109
+ }
110
+
111
+ private void createSummaryChannel() {
112
+ NotificationChannel channel = new NotificationChannel(
113
+ SUMMARY_CHANNEL_ID,
114
+ "Grouped notifications summary",
115
+ NotificationManager.IMPORTANCE_DEFAULT
116
+ );
117
+ notificationManager.createNotificationChannel(channel);
118
+ }
119
+
120
+ private void onReceived(Bundle bundle) {
121
+ String group = resolveGroup(bundle);
122
+ if (group == null) {
123
+ return;
124
+ }
125
+ awaitChildAndReconcile(group, notificationId(bundle), 0);
126
+ }
127
+
128
+ private void awaitChildAndReconcile(String group, int notificationId, int attempt) {
129
+ if (!enabled) {
130
+ return;
131
+ }
132
+ if (hasChild(group, notificationId) || attempt >= MAX_POST_ATTEMPTS) {
133
+ reconcile(group);
134
+ return;
135
+ }
136
+ handler.postDelayed(
137
+ () -> awaitChildAndReconcile(group, notificationId, attempt + 1),
138
+ RETRY_DELAY_MS
139
+ );
140
+ }
141
+
142
+ private void onRemoved(Bundle bundle) {
143
+ if (!enabled) {
144
+ return;
145
+ }
146
+ String group = resolveGroup(bundle);
147
+ if (group == null) {
148
+ return;
149
+ }
150
+ awaitRemovalAndReconcile(group, notificationId(bundle), 0);
151
+ }
152
+
153
+ private void awaitRemovalAndReconcile(String group, int notificationId, int attempt) {
154
+ if (!enabled) {
155
+ return;
156
+ }
157
+ if (hasChild(group, notificationId) && attempt < MAX_REMOVAL_ATTEMPTS) {
158
+ handler.postDelayed(
159
+ () -> awaitRemovalAndReconcile(group, notificationId, attempt + 1),
160
+ RETRY_DELAY_MS
161
+ );
162
+ return;
163
+ }
164
+ reconcile(group);
165
+ }
166
+
167
+ private String resolveGroup(Bundle bundle) {
168
+ return RetenoNotificationGroupingRuleProvider.resolveGroup(context, toStringMap(bundle));
169
+ }
170
+
171
+ private Map<String, String> toStringMap(Bundle bundle) {
172
+ Map<String, String> payload = new HashMap<>();
173
+ if (bundle == null) {
174
+ return payload;
175
+ }
176
+ for (String key : bundle.keySet()) {
177
+ Object value = bundle.get(key);
178
+ payload.put(key, value == null ? null : value.toString());
179
+ }
180
+ return payload;
181
+ }
182
+
183
+ private int notificationId(Bundle bundle) {
184
+ String interactionId = bundle == null ? null : bundle.getString("es_interaction_id");
185
+ return interactionId == null ? 1 : interactionId.hashCode();
186
+ }
187
+
188
+ private boolean hasChild(String group, int notificationId) {
189
+ for (StatusBarNotification sbn : notificationManager.getActiveNotifications()) {
190
+ if (sbn.getId() == notificationId && isChildInGroup(sbn.getNotification(), group)) {
191
+ return true;
192
+ }
193
+ }
194
+ return false;
195
+ }
196
+
197
+ private void reconcile(String group) {
198
+ if (!enabled) {
199
+ return;
200
+ }
201
+
202
+ int childCount = 0;
203
+ for (StatusBarNotification sbn : notificationManager.getActiveNotifications()) {
204
+ if (isChildInGroup(sbn.getNotification(), group)) {
205
+ childCount++;
206
+ }
207
+ }
208
+
209
+ String tag = summaryTag(group);
210
+ if (childCount < 2) {
211
+ notificationManager.cancel(tag, SUMMARY_NOTIFICATION_ID);
212
+ return;
213
+ }
214
+ if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS)
215
+ != PackageManager.PERMISSION_GRANTED) {
216
+ return;
217
+ }
218
+
219
+ Notification summary = new NotificationCompat.Builder(context, SUMMARY_CHANNEL_ID)
220
+ .setContentTitle("New notifications")
221
+ .setContentText("You have " + childCount + " new notifications")
222
+ .setSmallIcon(resolveSmallIcon())
223
+ .setGroup(group)
224
+ .setGroupSummary(true)
225
+ .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
226
+ .setOnlyAlertOnce(true)
227
+ .build();
228
+
229
+ notificationManager.notify(tag, SUMMARY_NOTIFICATION_ID, summary);
230
+ }
231
+
232
+ /**
233
+ * The launcher icon (context.getApplicationInfo().icon) is commonly an adaptive icon on modern
234
+ * apps, which is not a valid notification small icon and can make the system reject the
235
+ * notification. Reuse the plain vector drawable the Reteno Android SDK itself ships and falls
236
+ * back to (RetenoSdkPush's "reteno_default_push_icon"), which is guaranteed to be merged into
237
+ * any app that depends on the SDK, instead of ever touching the launcher icon.
238
+ */
239
+ private int resolveSmallIcon() {
240
+ int icon = context.getResources().getIdentifier(
241
+ "reteno_default_push_icon", "drawable", context.getPackageName()
242
+ );
243
+ return icon != 0 ? icon : android.R.drawable.ic_dialog_info;
244
+ }
245
+
246
+ private boolean isChildInGroup(Notification notification, String group) {
247
+ return group.equals(notification.getGroup()) && !NotificationCompat.isGroupSummary(notification);
248
+ }
249
+
250
+ private String summaryTag(String group) {
251
+ return SUMMARY_TAG_PREFIX + group;
252
+ }
253
+
254
+ private void cancelAllSummaries() {
255
+ for (StatusBarNotification sbn : notificationManager.getActiveNotifications()) {
256
+ String tag = sbn.getTag();
257
+ if (tag != null && tag.startsWith(SUMMARY_TAG_PREFIX)) {
258
+ notificationManager.cancel(tag, sbn.getId());
259
+ }
260
+ }
261
+ }
262
+ }
@@ -353,6 +353,25 @@ public class RetenoPlugin extends CordovaPlugin {
353
353
  return true;
354
354
  }
355
355
 
356
+ if ("setNotificationGroupingRule".equals(action)) {
357
+ Activity activity = cordova.getActivity();
358
+ if (activity == null) {
359
+ callbackContext.error("Reteno Android SDK Error: Activity is null.");
360
+ return true;
361
+ }
362
+ activity.runOnUiThread(new Runnable() {
363
+ @Override
364
+ public void run() {
365
+ try {
366
+ setNotificationGroupingRule(args, callbackContext);
367
+ } catch (Exception e) {
368
+ callbackContext.error("Reteno Android SDK Error: " + e.getLocalizedMessage());
369
+ }
370
+ }
371
+ });
372
+ return true;
373
+ }
374
+
356
375
  if ("getAppInboxMessages".equals(action)) {
357
376
  cordova.getThreadPool().execute(new Runnable() {
358
377
  @Override
@@ -608,7 +627,7 @@ public class RetenoPlugin extends CordovaPlugin {
608
627
  }
609
628
 
610
629
  /**
611
- * Register notification event listeners using the SDK 2.9.4 EventListener API.
630
+ * Register notification event listeners using the SDK 2.9.6 EventListener API.
612
631
  * Each listener is a direct {@link Procedure} implementation — no reflection needed.
613
632
  */
614
633
  private void registerNotificationListeners() {
@@ -1181,7 +1200,7 @@ public class RetenoPlugin extends CordovaPlugin {
1181
1200
  ? payload.optBoolean("sessionStartEventsEnabled", legacySessionEventsEnabled)
1182
1201
  : legacySessionEventsEnabled;
1183
1202
 
1184
- // SDK 2.9.4 default for session end is disabled unless explicitly enabled.
1203
+ // SDK default for session end is disabled unless explicitly enabled.
1185
1204
  boolean sessionEndEventsEnabled = payload.has("sessionEndEventsEnabled")
1186
1205
  ? payload.optBoolean("sessionEndEventsEnabled", false)
1187
1206
  : (payload.has("sessionEventsEnabled") ? legacySessionEventsEnabled : false);
@@ -1320,6 +1339,61 @@ public class RetenoPlugin extends CordovaPlugin {
1320
1339
  }
1321
1340
  }
1322
1341
 
1342
+ private void setNotificationGroupingRule(JSONArray args, CallbackContext callbackContext) {
1343
+ Object arg0 = (args == null || args.length() == 0) ? null : args.opt(0);
1344
+ if (arg0 instanceof JSONArray) {
1345
+ arg0 = ((JSONArray) arg0).opt(0);
1346
+ }
1347
+
1348
+ if (arg0 == null || arg0 == JSONObject.NULL) {
1349
+ RetenoNotificationGroupingRuleProvider.configure(
1350
+ cordova.getActivity().getApplicationContext(), null, null, false
1351
+ );
1352
+ callbackContext.success(1);
1353
+ return;
1354
+ }
1355
+
1356
+ if (!(arg0 instanceof JSONObject)) {
1357
+ callbackContext.error("Invalid argument: expected null or an object with payloadKey or groupId");
1358
+ return;
1359
+ }
1360
+
1361
+ JSONObject config = (JSONObject) arg0;
1362
+ Object rawPayloadKey = config.opt("payloadKey");
1363
+ Object rawGroupId = config.opt("groupId");
1364
+ Object rawShowSummary = config.opt("showSummary");
1365
+ if ((rawPayloadKey != null && rawPayloadKey != JSONObject.NULL && !(rawPayloadKey instanceof String))
1366
+ || (rawGroupId != null && rawGroupId != JSONObject.NULL && !(rawGroupId instanceof String))) {
1367
+ callbackContext.error("Invalid argument: payloadKey and groupId must be strings");
1368
+ return;
1369
+ }
1370
+ if (rawShowSummary != null && rawShowSummary != JSONObject.NULL
1371
+ && !(rawShowSummary instanceof Boolean)) {
1372
+ callbackContext.error("Invalid argument: showSummary must be a boolean");
1373
+ return;
1374
+ }
1375
+
1376
+ String payloadKey = rawPayloadKey instanceof String ? (String) rawPayloadKey : null;
1377
+ String groupId = rawGroupId instanceof String ? (String) rawGroupId : null;
1378
+ payloadKey = payloadKey == null ? null : payloadKey.trim();
1379
+ groupId = groupId == null ? null : groupId.trim();
1380
+
1381
+ boolean hasPayloadKey = !TextUtils.isEmpty(payloadKey);
1382
+ boolean hasGroupId = !TextUtils.isEmpty(groupId);
1383
+ if (hasPayloadKey == hasGroupId) {
1384
+ callbackContext.error("Invalid argument: provide exactly one of payloadKey or groupId");
1385
+ return;
1386
+ }
1387
+
1388
+ RetenoNotificationGroupingRuleProvider.configure(
1389
+ cordova.getActivity().getApplicationContext(),
1390
+ hasPayloadKey ? payloadKey : null,
1391
+ hasGroupId ? groupId : null,
1392
+ rawShowSummary instanceof Boolean && (Boolean) rawShowSummary
1393
+ );
1394
+ callbackContext.success(1);
1395
+ }
1396
+
1323
1397
  private void getAppInboxMessages(JSONArray args, CallbackContext callbackContext) throws JSONException {
1324
1398
  if (args == null || args.length() == 0) {
1325
1399
  callbackContext.error("Missing argument: payload");
@@ -5,7 +5,7 @@ allprojects {
5
5
  }
6
6
  }
7
7
 
8
- def retenoFcmVersion = '2.9.4'
8
+ def retenoFcmVersion = '2.10.1'
9
9
 
10
10
  def firebaseMessagingVersion = '23.1.0'
11
11