react-native-insider 8.0.0 → 8.0.1-nh

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/RNInsider.podspec CHANGED
@@ -9,12 +9,19 @@ Pod::Spec.new do |s|
9
9
  s.authors = package_json['author']
10
10
  s.license = 'MIT'
11
11
  s.platform = :ios, '12.0'
12
- s.source = {:http => 'https://mobilesdk.useinsider.com/iOS/15.0.0/InsiderMobileIOSFramework.zip'}
12
+ s.source = {:http => 'https://mobilesdk.useinsider.com/iOS/15.0.1/InsiderMobileIOSFramework.zip'}
13
13
  s.source_files = 'ios/RNInsider/*.{h,m}'
14
14
  s.requires_arc = true
15
15
  s.static_framework = true
16
16
  s.dependency 'React'
17
- s.dependency 'InsiderMobile', '15.0.0'
17
+ s.dependency 'InsiderMobile', '15.0.1'
18
18
  s.dependency 'InsiderGeofence', '1.2.4'
19
19
  s.dependency 'InsiderHybrid', '1.7.6'
20
+
21
+ s.test_spec 'Tests' do |test_spec|
22
+ test_spec.source_files = 'ios/RNInsiderTests/**/*.{h,m}'
23
+ test_spec.frameworks = 'XCTest'
24
+ test_spec.requires_app_host = false
25
+ test_spec.dependency 'OCMock', '~> 3.9'
26
+ end
20
27
  end
@@ -2,7 +2,6 @@ buildscript {
2
2
  repositories {
3
3
  google()
4
4
  mavenCentral()
5
- maven { url "https://developer.huawei.com/repo/"}
6
5
  }
7
6
 
8
7
  dependencies {
@@ -30,19 +29,23 @@ android {
30
29
  if (major >= 8) {
31
30
  namespace "com.useinsider.react"
32
31
  }
32
+
33
+ testOptions {
34
+ unitTests.includeAndroidResources = true
35
+ unitTests.returnDefaultValues = true
36
+ }
33
37
  }
34
38
 
35
39
  repositories {
36
40
  google()
37
41
  mavenCentral()
38
- maven { url "https://developer.huawei.com/repo/"}
39
42
  maven { url "https://mobilesdk.useinsider.com/android" }
40
43
  }
41
44
 
42
45
 
43
46
  dependencies {
44
47
  implementation "com.facebook.react:react-native:${getVersionFromPartner('reactNativeVersion', '+')}"
45
- implementation 'com.useinsider:insider:16.0.1'
48
+ implementation 'com.useinsider:insider:16.0.1-nh'
46
49
  implementation 'com.useinsider:insiderhybrid:1.3.4'
47
50
 
48
51
  implementation 'androidx.security:security-crypto:1.1.0-alpha06'
@@ -57,4 +60,8 @@ dependencies {
57
60
  implementation 'com.huawei.hms:push:6.13.0.300'
58
61
  implementation 'com.huawei.hms:ads-identifier:3.4.62.300'
59
62
  implementation 'com.huawei.hms:location:6.16.0.302'
63
+
64
+ testImplementation 'junit:junit:4.13.2'
65
+ testImplementation 'org.mockito:mockito-core:5.12.0'
66
+ testImplementation 'org.mockito:mockito-inline:5.2.0'
60
67
  }
@@ -895,10 +895,6 @@ public class RNInsiderModule extends ReactContextBaseJavaModule {
895
895
  }
896
896
  String provider = Insider.Instance.getCurrentProvider(reactContext);
897
897
  switch (provider) {
898
- case "huawei":
899
- com.huawei.hms.push.RemoteMessage hmsRemoteMessage = new com.huawei.hms.push.RemoteMessage.Builder("insider").setData(remoteMessageStringMap).build();
900
- Insider.Instance.handleHMSNotification(reactContext, hmsRemoteMessage);
901
- break;
902
898
  case "other":
903
899
  case "google":
904
900
  RemoteMessage fcmRemoteMessage = new RemoteMessage.Builder("insider").setData(remoteMessageStringMap).build();
@@ -913,6 +909,22 @@ public class RNInsiderModule extends ReactContextBaseJavaModule {
913
909
  }
914
910
  }
915
911
 
912
+ @ReactMethod
913
+ public void triggerPushProcessWithNotificationData(ReadableMap remoteMessageData) {
914
+ try {
915
+ if (remoteMessageData == null)
916
+ return;
917
+ Map<String, Object> convertedMap = RNUtils.convertReadableMapToMap(remoteMessageData);
918
+ Map<String, String> remoteMessageStringMap = new HashMap<>();
919
+ for (String key : convertedMap.keySet()) {
920
+ remoteMessageStringMap.put(key, String.valueOf(convertedMap.get(key)));
921
+ }
922
+ Insider.Instance.triggerPushProcessWithNotificationData(reactContext, remoteMessageStringMap);
923
+ } catch (Exception e) {
924
+ Insider.Instance.putException(e);
925
+ }
926
+ }
927
+
916
928
  // Deprecated
917
929
  @ReactMethod
918
930
  public void enableIDFACollection(final boolean enableIDFACollection) {}
@@ -0,0 +1,169 @@
1
+ package com.useinsider.react;
2
+
3
+ import static org.junit.Assert.assertEquals;
4
+ import static org.junit.Assert.assertTrue;
5
+ import static org.mockito.ArgumentMatchers.any;
6
+ import static org.mockito.ArgumentMatchers.eq;
7
+ import static org.mockito.Mockito.never;
8
+ import static org.mockito.Mockito.times;
9
+ import static org.mockito.Mockito.verify;
10
+ import static org.mockito.Mockito.when;
11
+
12
+ import com.facebook.react.bridge.ReactApplicationContext;
13
+ import com.facebook.react.bridge.ReadableMap;
14
+ import com.facebook.react.bridge.ReadableMapKeySetIterator;
15
+ import com.facebook.react.bridge.ReadableType;
16
+ import com.useinsider.insider.Insider;
17
+
18
+ import org.junit.After;
19
+ import org.junit.Before;
20
+ import org.junit.Test;
21
+ import org.mockito.ArgumentCaptor;
22
+
23
+ import java.lang.reflect.Field;
24
+ import java.util.HashMap;
25
+ import java.util.Map;
26
+
27
+ /**
28
+ * Unit tests for {@link RNInsiderModule#triggerPushProcessWithNotificationData(ReadableMap)}.
29
+ *
30
+ * MOB-26455: New bridge method that converts the JS notification payload to a
31
+ * String map and forwards it to {@code Insider.Instance.triggerPushProcessWithNotificationData}.
32
+ * The bridge must:
33
+ * - Treat a null payload as a no-op (early return, no native call).
34
+ * - Convert ReadableMap entries to String values via {@link String#valueOf(Object)}.
35
+ * - Pass the supplied {@link ReactApplicationContext} to the native SDK.
36
+ * - Route any thrown exception through {@code Insider.Instance.putException}
37
+ * instead of propagating it to the JS caller.
38
+ */
39
+ public class RNInsiderModuleTest {
40
+
41
+ private RNInsiderModule module;
42
+ private ReactApplicationContext reactContext;
43
+ private Insider insiderMock;
44
+ private Insider originalInstance;
45
+
46
+ @Before
47
+ public void setUp() throws Exception {
48
+ reactContext = org.mockito.Mockito.mock(ReactApplicationContext.class);
49
+ insiderMock = org.mockito.Mockito.mock(Insider.class);
50
+
51
+ Field instanceField = Insider.class.getDeclaredField("Instance");
52
+ instanceField.setAccessible(true);
53
+ originalInstance = (Insider) instanceField.get(null);
54
+ instanceField.set(null, insiderMock);
55
+
56
+ module = new RNInsiderModule(reactContext);
57
+ }
58
+
59
+ @After
60
+ public void tearDown() throws Exception {
61
+ Field instanceField = Insider.class.getDeclaredField("Instance");
62
+ instanceField.setAccessible(true);
63
+ instanceField.set(null, originalInstance);
64
+ }
65
+
66
+ @Test
67
+ public void triggerPushProcessWithNotificationData_nullPayload_doesNotInvokeNativeSdk() {
68
+ module.triggerPushProcessWithNotificationData(null);
69
+
70
+ verify(insiderMock, never())
71
+ .triggerPushProcessWithNotificationData(any(), any());
72
+ verify(insiderMock, never()).putException(any(Exception.class));
73
+ }
74
+
75
+ @Test
76
+ public void triggerPushProcessWithNotificationData_emptyMap_forwardsEmptyStringMap() {
77
+ ReadableMap empty = readableMapOf(new HashMap<String, Object>());
78
+
79
+ module.triggerPushProcessWithNotificationData(empty);
80
+
81
+ @SuppressWarnings("unchecked")
82
+ ArgumentCaptor<Map<String, String>> captor = ArgumentCaptor.forClass(Map.class);
83
+ verify(insiderMock, times(1))
84
+ .triggerPushProcessWithNotificationData(eq(reactContext), captor.capture());
85
+ assertTrue("Empty payload should produce empty String map", captor.getValue().isEmpty());
86
+ }
87
+
88
+ @Test
89
+ public void triggerPushProcessWithNotificationData_stringValues_passedThroughVerbatim() {
90
+ Map<String, Object> entries = new HashMap<>();
91
+ entries.put("title", "Welcome");
92
+ entries.put("body", "Hello there");
93
+ ReadableMap payload = readableMapOf(entries);
94
+
95
+ module.triggerPushProcessWithNotificationData(payload);
96
+
97
+ @SuppressWarnings("unchecked")
98
+ ArgumentCaptor<Map<String, String>> captor = ArgumentCaptor.forClass(Map.class);
99
+ verify(insiderMock, times(1))
100
+ .triggerPushProcessWithNotificationData(eq(reactContext), captor.capture());
101
+ Map<String, String> forwarded = captor.getValue();
102
+ assertEquals("Welcome", forwarded.get("title"));
103
+ assertEquals("Hello there", forwarded.get("body"));
104
+ assertEquals(2, forwarded.size());
105
+ }
106
+
107
+ @Test
108
+ public void triggerPushProcessWithNotificationData_numericValues_stringifiedViaStringValueOf() {
109
+ Map<String, Object> entries = new HashMap<>();
110
+ entries.put("priority", 5.0);
111
+ entries.put("retry", true);
112
+ ReadableMap payload = readableMapOf(entries);
113
+
114
+ module.triggerPushProcessWithNotificationData(payload);
115
+
116
+ @SuppressWarnings("unchecked")
117
+ ArgumentCaptor<Map<String, String>> captor = ArgumentCaptor.forClass(Map.class);
118
+ verify(insiderMock, times(1))
119
+ .triggerPushProcessWithNotificationData(eq(reactContext), captor.capture());
120
+ Map<String, String> forwarded = captor.getValue();
121
+ assertEquals(String.valueOf(5.0), forwarded.get("priority"));
122
+ assertEquals("true", forwarded.get("retry"));
123
+ }
124
+
125
+ @Test
126
+ public void triggerPushProcessWithNotificationData_nativeSdkThrows_swallowedViaPutException() {
127
+ ReadableMap payload = readableMapOf(new HashMap<String, Object>());
128
+ RuntimeException boom = new RuntimeException("native failure");
129
+ org.mockito.Mockito.doThrow(boom)
130
+ .when(insiderMock)
131
+ .triggerPushProcessWithNotificationData(any(), any());
132
+
133
+ module.triggerPushProcessWithNotificationData(payload);
134
+
135
+ verify(insiderMock, times(1)).putException(boom);
136
+ }
137
+
138
+ /**
139
+ * Builds a {@link ReadableMap} stub backed by a plain Java map. The bridge
140
+ * only walks the iterator and asks for {@code getType}/{@code getString}/
141
+ * {@code getDouble}/{@code getBoolean}, so we mirror that minimal contract
142
+ * instead of pulling in {@code JavaOnlyMap} (which requires the React Native
143
+ * AAR at test time).
144
+ */
145
+ private static ReadableMap readableMapOf(Map<String, Object> entries) {
146
+ ReadableMap map = org.mockito.Mockito.mock(ReadableMap.class);
147
+ ReadableMapKeySetIterator iterator = org.mockito.Mockito.mock(ReadableMapKeySetIterator.class);
148
+ Iterable<String> keys = new java.util.ArrayList<>(entries.keySet());
149
+ java.util.Iterator<String> keyIter = keys.iterator();
150
+ when(iterator.hasNextKey()).thenAnswer(invocation -> keyIter.hasNext());
151
+ when(iterator.nextKey()).thenAnswer(invocation -> keyIter.next());
152
+ when(map.keySetIterator()).thenReturn(iterator);
153
+ for (Map.Entry<String, Object> entry : entries.entrySet()) {
154
+ String key = entry.getKey();
155
+ Object value = entry.getValue();
156
+ if (value instanceof String) {
157
+ when(map.getType(key)).thenReturn(ReadableType.String);
158
+ when(map.getString(key)).thenReturn((String) value);
159
+ } else if (value instanceof Boolean) {
160
+ when(map.getType(key)).thenReturn(ReadableType.Boolean);
161
+ when(map.getBoolean(key)).thenReturn((Boolean) value);
162
+ } else if (value instanceof Number) {
163
+ when(map.getType(key)).thenReturn(ReadableType.Number);
164
+ when(map.getDouble(key)).thenReturn(((Number) value).doubleValue());
165
+ }
166
+ }
167
+ return map;
168
+ }
169
+ }
package/index.d.ts CHANGED
@@ -202,6 +202,7 @@ declare module 'react-native-insider' {
202
202
  static startTrackingGeofence(): void;
203
203
  static setAllowsBackgroundLocationUpdates(allowsBackgroundLocationUpdates: boolean): void;
204
204
  static handleNotification(notification: any): void;
205
+ static triggerPushProcessWithNotificationData(notification: any): void;
205
206
  static enableIDFACollection(enableIDFACollection: boolean): void;
206
207
  static removeInapp(): void;
207
208
  static registerWithQuietPermission(enabled: boolean): void;
package/index.js CHANGED
@@ -744,6 +744,19 @@ export default class RNInsider {
744
744
  }
745
745
  }
746
746
 
747
+ static triggerPushProcessWithNotificationData(notification) {
748
+ if (shouldNotProceed()) return;
749
+ if (checkParameters([{ type: 'object', value: notification }])) {
750
+ showParameterWarningLog("triggerPushProcessWithNotificationData", [{ type: 'object', value: notification }]);
751
+ return;
752
+ }
753
+ try {
754
+ Insider.triggerPushProcessWithNotificationData(notification);
755
+ } catch (error) {
756
+ Insider.putErrorLog(generateJSONErrorString(error));
757
+ }
758
+ }
759
+
747
760
  static setHybridPushToken(token) {
748
761
  if (shouldNotProceed()) return;
749
762
  if (checkParameters([{ type: 'string', value: token }])) {
@@ -744,6 +744,10 @@ RCT_EXPORT_METHOD(handleNotification:(NSDictionary *)notification) {
744
744
  }
745
745
  }
746
746
 
747
+ RCT_EXPORT_METHOD(triggerPushProcessWithNotificationData:(NSDictionary *)notification) {
748
+ [self handleNotification:notification];
749
+ }
750
+
747
751
  RCT_EXPORT_METHOD(enableIDFACollection:(BOOL)enableIDFACollection) {
748
752
  @try {
749
753
  [Insider enableIDFACollection:enableIDFACollection];
@@ -0,0 +1,91 @@
1
+ // Unit tests for `triggerPushProcessWithNotificationData:` on the iOS bridge.
2
+ //
3
+ // MOB-26455: The new method delegates straight to `handleNotification:`, which
4
+ // is responsible for stamping `aps = "insider"` and forwarding the payload to
5
+ // the native InsiderMobile entry points. These tests pin that contract so
6
+ // future refactors cannot silently drop the delegation or skip the stamp.
7
+
8
+ #import <XCTest/XCTest.h>
9
+ #import <OCMock/OCMock.h>
10
+
11
+ #import "RNInsider.h"
12
+
13
+ #if __has_include(<InsiderMobile/Insider.h>)
14
+ #import <InsiderMobile/Insider.h>
15
+ #elif __has_include("Insider.h")
16
+ #import "Insider.h"
17
+ #endif
18
+
19
+ @interface RNInsiderTriggerPushProcessTests : XCTestCase
20
+ @property (nonatomic, strong) RNInsider *bridge;
21
+ @property (nonatomic, strong) id insiderClassMock;
22
+ @end
23
+
24
+ @implementation RNInsiderTriggerPushProcessTests
25
+
26
+ - (void)setUp {
27
+ [super setUp];
28
+ self.bridge = [RNInsider new];
29
+ self.insiderClassMock = OCMClassMock([Insider class]);
30
+ }
31
+
32
+ - (void)tearDown {
33
+ [self.insiderClassMock stopMocking];
34
+ self.insiderClassMock = nil;
35
+ self.bridge = nil;
36
+ [super tearDown];
37
+ }
38
+
39
+ - (void)testTriggerPushProcess_delegatesToHandleNotification {
40
+ id partialBridge = OCMPartialMock(self.bridge);
41
+ NSDictionary *payload = @{ @"title": @"Hi", @"body": @"There" };
42
+
43
+ OCMExpect([partialBridge handleNotification:payload]);
44
+
45
+ [self.bridge triggerPushProcessWithNotificationData:payload];
46
+
47
+ OCMVerifyAll(partialBridge);
48
+ [partialBridge stopMocking];
49
+ }
50
+
51
+ - (void)testTriggerPushProcess_stampsApsKeyAndForwardsToInsider {
52
+ NSDictionary *payload = @{ @"title": @"Welcome" };
53
+
54
+ [self.bridge triggerPushProcessWithNotificationData:payload];
55
+
56
+ OCMVerify([self.insiderClassMock handlePushLogWithUserInfo:[OCMArg checkWithBlock:^BOOL(NSDictionary *arg) {
57
+ return [arg[@"aps"] isEqualToString:@"insider"] && [arg[@"title"] isEqualToString:@"Welcome"];
58
+ }]]);
59
+ OCMVerify([self.insiderClassMock trackInteractiveLogWithUserInfo:[OCMArg checkWithBlock:^BOOL(NSDictionary *arg) {
60
+ return [arg[@"aps"] isEqualToString:@"insider"] && [arg[@"title"] isEqualToString:@"Welcome"];
61
+ }]]);
62
+ }
63
+
64
+ - (void)testTriggerPushProcess_emptyDictionary_stillStampsApsKey {
65
+ [self.bridge triggerPushProcessWithNotificationData:@{}];
66
+
67
+ OCMVerify([self.insiderClassMock handlePushLogWithUserInfo:[OCMArg checkWithBlock:^BOOL(NSDictionary *arg) {
68
+ return [arg[@"aps"] isEqualToString:@"insider"];
69
+ }]]);
70
+ }
71
+
72
+ - (void)testTriggerPushProcess_doesNotMutateOriginalDictionary {
73
+ NSDictionary *payload = @{ @"title": @"Hi" };
74
+
75
+ [self.bridge triggerPushProcessWithNotificationData:payload];
76
+
77
+ XCTAssertNil(payload[@"aps"], @"Original payload must remain untouched");
78
+ XCTAssertEqualObjects(payload[@"title"], @"Hi");
79
+ }
80
+
81
+ - (void)testTriggerPushProcess_swallowsNativeException {
82
+ OCMStub([self.insiderClassMock handlePushLogWithUserInfo:[OCMArg any]])
83
+ .andThrow([NSException exceptionWithName:@"BoomException" reason:@"native failure" userInfo:nil]);
84
+
85
+ XCTAssertNoThrow([self.bridge triggerPushProcessWithNotificationData:@{ @"title": @"Hi" }],
86
+ @"Bridge must catch native exceptions and route them through Insider sendError");
87
+
88
+ OCMVerify([self.insiderClassMock sendError:[OCMArg any] desc:[OCMArg any]]);
89
+ }
90
+
91
+ @end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-insider",
3
- "version": "8.0.0",
3
+ "version": "8.0.1-nh",
4
4
  "description": "React Native Insider SDK",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -29,7 +29,7 @@ export class InsiderAppCardImage {
29
29
  */
30
30
  export class InsiderAppCardButton {
31
31
  /** Unique identifier for the button. */
32
- readonly buttonId: string;
32
+ readonly id: string;
33
33
 
34
34
  /** Identifier of the app card this button belongs to. */
35
35
  readonly appCardId: string;
@@ -122,7 +122,7 @@ export type InsiderAppCardType = "message" | "image"
122
122
  */
123
123
  export class InsiderAppCard {
124
124
  /** Unique identifier for the app card. */
125
- readonly appCardId: string;
125
+ readonly id: string;
126
126
 
127
127
  /** Type of the app card (text or image). */
128
128
  readonly type: InsiderAppCardType;
@@ -33,21 +33,21 @@ class InsiderAppCardImage {
33
33
 
34
34
  class InsiderAppCardButton {
35
35
  #data;
36
- #buttonId;
36
+ #id;
37
37
  #appCardId;
38
38
  #text;
39
39
  #action;
40
40
 
41
41
  constructor(appCardId, data) {
42
42
  this.#data = data;
43
- this.#buttonId = data.id;
43
+ this.#id = data.id;
44
44
  this.#appCardId = appCardId;
45
45
  this.#text = data.text;
46
46
  this.#action = InsiderAppCardAction.fromData(data.action);
47
47
  }
48
48
 
49
- get buttonId() {
50
- return this.#buttonId;
49
+ get id() {
50
+ return this.#id;
51
51
  }
52
52
 
53
53
  get appCardId() {
@@ -164,7 +164,7 @@ class InsiderAppCardFeedbackAction extends InsiderAppCardAction {
164
164
 
165
165
  class InsiderAppCard {
166
166
  #data;
167
- #appCardId;
167
+ #id;
168
168
  #type;
169
169
  #isRead;
170
170
  #images;
@@ -174,7 +174,7 @@ class InsiderAppCard {
174
174
 
175
175
  constructor(data) {
176
176
  this.#data = data;
177
- this.#appCardId = data.id;
177
+ this.#id = data.id;
178
178
  this.#type = data.type;
179
179
  this.#isRead = data.read;
180
180
  this.#images = data.images ? data.images.map(x => new InsiderAppCardImage(x)) : null;
@@ -183,8 +183,8 @@ class InsiderAppCard {
183
183
  this.#action = InsiderAppCardAction.fromData(data.action);
184
184
  }
185
185
 
186
- get appCardId() {
187
- return this.#appCardId;
186
+ get id() {
187
+ return this.#id;
188
188
  }
189
189
 
190
190
  get type() {
@@ -217,7 +217,7 @@ class InsiderAppCard {
217
217
 
218
218
  markAsRead(completion) {
219
219
  const promise = new Promise((resolve, reject) => {
220
- InsiderAppCards.markAsRead([this.appCardId], error => {
220
+ InsiderAppCards.markAsRead([this.id], error => {
221
221
  if (error) reject(error);
222
222
  else {
223
223
  this.#isRead = true;
@@ -231,7 +231,7 @@ class InsiderAppCard {
231
231
 
232
232
  markAsUnread(completion) {
233
233
  const promise = new Promise((resolve, reject) => {
234
- InsiderAppCards.markAsUnread([this.appCardId], error => {
234
+ InsiderAppCards.markAsUnread([this.id], error => {
235
235
  if (error) reject(error);
236
236
  else {
237
237
  this.#isRead = false;
@@ -245,7 +245,7 @@ class InsiderAppCard {
245
245
 
246
246
  delete(completion) {
247
247
  const promise = new Promise((resolve, reject) => {
248
- InsiderAppCards.delete([this.appCardId], error => {
248
+ InsiderAppCards.delete([this.id], error => {
249
249
  if (error) reject(error);
250
250
  else resolve();
251
251
  });
@@ -42,7 +42,7 @@ interface InsiderAppCards {
42
42
  * console.log('Failed to fetch campaigns:', error.message);
43
43
  * } else {
44
44
  * campaignResponse.appCards.forEach(appCard => {
45
- * console.log('App Card Id:', appCard.appCardId, 'Read:', appCard.isRead);
45
+ * console.log('App Card Id:', appCard.id, 'Read:', appCard.isRead);
46
46
  * });
47
47
  * }
48
48
  * });
@@ -63,7 +63,7 @@ interface InsiderAppCards {
63
63
  * try {
64
64
  * const campaignResponse = await appCards.getCampaigns();
65
65
  * campaignResponse.appCards.forEach(appCard => {
66
- * console.log('App Card Id:', appCard.appCardId, 'Read:', appCard.isRead);
66
+ * console.log('App Card Id:', appCard.id, 'Read:', appCard.isRead);
67
67
  * });
68
68
  * } catch (error) {
69
69
  * console.log('Failed to fetch campaigns:', error.message);