react-native-notify-kit 10.2.0 → 10.3.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.
Files changed (61) hide show
  1. package/README.md +6 -6
  2. package/android/src/main/AndroidManifest.xml +0 -1
  3. package/android/src/main/java/app/notifee/core/ForegroundService.java +3 -1
  4. package/android/src/main/java/app/notifee/core/NotifeeAlarmManager.java +4 -2
  5. package/android/src/main/java/app/notifee/core/NotificationManager.java +43 -43
  6. package/android/src/main/java/app/notifee/core/ReceiverService.java +18 -13
  7. package/android/src/main/java/app/notifee/core/database/NotifeeCoreDatabase.java +1 -4
  8. package/android/src/main/java/app/notifee/core/model/ChannelModel.java +8 -13
  9. package/android/src/main/java/app/notifee/core/model/IntervalTriggerModel.java +2 -6
  10. package/android/src/main/java/app/notifee/core/model/NotificationAndroidActionModel.java +1 -2
  11. package/android/src/main/java/app/notifee/core/model/NotificationAndroidModel.java +30 -33
  12. package/android/src/main/java/app/notifee/core/model/NotificationAndroidPressActionModel.java +3 -9
  13. package/android/src/main/java/app/notifee/core/model/NotificationAndroidStyleModel.java +6 -4
  14. package/android/src/main/java/app/notifee/core/model/TimestampTriggerModel.java +7 -4
  15. package/android/src/main/java/app/notifee/core/utility/BundleValueReader.java +65 -0
  16. package/android/src/main/java/app/notifee/core/utility/IntentUtils.java +0 -28
  17. package/android/src/main/java/app/notifee/core/utility/ObjectUtils.java +1 -1
  18. package/android/src/main/java/app/notifee/core/utility/ParcelableCompatReader.java +64 -0
  19. package/android/src/main/java/app/notifee/core/utility/PowerManagerUtils.java +79 -41
  20. package/android/src/main/java/app/notifee/core/utility/ResourceUtils.java +3 -0
  21. package/android/src/main/kotlin/io/invertase/notifee/NotifeeApiModule.kt +1 -1
  22. package/android/src/main/kotlin/io/invertase/notifee/NotifeeInitProvider.kt +0 -26
  23. package/android/src/main/kotlin/io/invertase/notifee/NotifeeReactUtils.kt +1 -2
  24. package/android/src/test/java/app/notifee/core/NotifeeAlarmManagerCurrentBehaviorTest.java +446 -0
  25. package/android/src/test/java/app/notifee/core/NotificationManagerCreateTriggerNotificationCurrentBehaviorTest.java +203 -0
  26. package/android/src/test/java/app/notifee/core/NotificationManagerPressActionOptOutTest.java +180 -0
  27. package/android/src/test/java/app/notifee/core/NotificationReceiverHandlerInitialNotificationTest.java +157 -0
  28. package/android/src/test/java/app/notifee/core/ReceiverServiceInitialNotificationTest.java +169 -0
  29. package/android/src/test/java/app/notifee/core/model/ChannelModelTest.java +116 -0
  30. package/android/src/test/java/app/notifee/core/model/IntervalTriggerModelTest.java +71 -0
  31. package/android/src/test/java/app/notifee/core/model/NotificationAndroidModelTest.java +309 -0
  32. package/android/src/test/java/app/notifee/core/model/NotificationAndroidStyleModelTest.java +154 -0
  33. package/android/src/test/java/app/notifee/core/model/TimestampTriggerModelTest.java +296 -14
  34. package/android/src/test/java/app/notifee/core/utility/BundleValueReaderTest.java +131 -0
  35. package/android/src/test/java/app/notifee/core/utility/ObjectUtilsTest.java +142 -0
  36. package/android/src/test/java/app/notifee/core/utility/ParcelableCompatReaderTest.java +124 -0
  37. package/android/src/test/java/io/invertase/notifee/HeadlessTaskReactHostTest.java +136 -0
  38. package/android/src/test/java/io/invertase/notifee/NotifeeEventSubscriberRoutingTest.java +262 -0
  39. package/android/src/test/java/io/invertase/notifee/NotifeeReactUtilsDrawerTest.java +24 -0
  40. package/cli/dist/lib/patchPodfile.d.ts +3 -1
  41. package/cli/dist/lib/patchPodfile.js +125 -33
  42. package/cli/dist/lib/patchPodfile.js.map +1 -1
  43. package/dist/index.d.ts +2 -0
  44. package/dist/types/Module.d.ts +20 -6
  45. package/dist/types/Notification.d.ts +5 -2
  46. package/dist/types/Notification.js +5 -2
  47. package/dist/types/Notification.js.map +1 -1
  48. package/dist/types/NotificationAndroid.d.ts +4 -0
  49. package/dist/types/NotificationAndroid.js +4 -0
  50. package/dist/types/NotificationAndroid.js.map +1 -1
  51. package/dist/types/PowerManagerInfo.d.ts +9 -4
  52. package/dist/version.d.ts +1 -1
  53. package/dist/version.js +1 -1
  54. package/ios/NotifeeCore/NotifeeCore.m +101 -114
  55. package/package.json +1 -1
  56. package/src/index.ts +2 -0
  57. package/src/types/Module.ts +20 -6
  58. package/src/types/Notification.ts +5 -2
  59. package/src/types/NotificationAndroid.ts +4 -0
  60. package/src/types/PowerManagerInfo.ts +9 -4
  61. package/src/version.ts +1 -1
@@ -0,0 +1,124 @@
1
+ package app.notifee.core.utility;
2
+
3
+ import static org.junit.Assert.assertEquals;
4
+ import static org.junit.Assert.assertNotNull;
5
+ import static org.junit.Assert.assertNull;
6
+ import static org.junit.Assert.assertThrows;
7
+
8
+ import android.app.Notification;
9
+ import android.os.Bundle;
10
+ import java.util.ArrayList;
11
+ import org.junit.Test;
12
+ import org.junit.runner.RunWith;
13
+ import org.robolectric.RobolectricTestRunner;
14
+ import org.robolectric.RuntimeEnvironment;
15
+ import org.robolectric.annotation.Config;
16
+
17
+ @RunWith(RobolectricTestRunner.class)
18
+ public class ParcelableCompatReaderTest {
19
+
20
+ @Test
21
+ @Config(sdk = 32)
22
+ public void getParcelable_legacyPath_returnsNotificationAndNullWhenMissing() {
23
+ Bundle bundle = new Bundle();
24
+ Notification notification = createNotification();
25
+ bundle.putParcelable("notification", notification);
26
+ bundle.putParcelable("explicitNull", (Notification) null);
27
+
28
+ assertEquals(
29
+ notification,
30
+ ParcelableCompatReader.getParcelable(bundle, "notification", Notification.class));
31
+ assertNull(ParcelableCompatReader.getParcelable(bundle, "missing", Notification.class));
32
+ assertNull(ParcelableCompatReader.getParcelable(bundle, "explicitNull", Notification.class));
33
+ }
34
+
35
+ @Test
36
+ @Config(sdk = 33)
37
+ public void getParcelable_typedPath_returnsNotificationAndNullWhenMissing() {
38
+ Bundle bundle = new Bundle();
39
+ Notification notification = createNotification();
40
+ bundle.putParcelable("notification", notification);
41
+ bundle.putParcelable("explicitNull", (Notification) null);
42
+
43
+ assertEquals(
44
+ notification,
45
+ ParcelableCompatReader.getParcelable(bundle, "notification", Notification.class));
46
+ assertNull(ParcelableCompatReader.getParcelable(bundle, "missing", Notification.class));
47
+ assertNull(ParcelableCompatReader.getParcelable(bundle, "explicitNull", Notification.class));
48
+ }
49
+
50
+ @Test
51
+ @Config(sdk = 32)
52
+ public void getParcelable_legacyPath_preservesWrongClassCast() {
53
+ Bundle bundle = new Bundle();
54
+ bundle.putParcelable("notification", createNotification());
55
+
56
+ assertThrows(
57
+ ClassCastException.class,
58
+ () -> {
59
+ Bundle ignored =
60
+ ParcelableCompatReader.getParcelable(bundle, "notification", Bundle.class);
61
+ });
62
+ }
63
+
64
+ @Test
65
+ @Config(sdk = 33)
66
+ public void getParcelable_typedPath_returnsNullForWrongClass() {
67
+ Bundle bundle = new Bundle();
68
+ bundle.putParcelable("notification", createNotification());
69
+
70
+ assertNull(ParcelableCompatReader.getParcelable(bundle, "notification", Bundle.class));
71
+ }
72
+
73
+ @Test
74
+ @Config(sdk = 32)
75
+ public void getParcelableArrayList_legacyPath_returnsBundleList() {
76
+ Bundle bundle = new Bundle();
77
+ ArrayList<Bundle> bundles = createBundleList();
78
+ bundle.putParcelableArrayList("bundles", bundles);
79
+ bundle.putParcelableArrayList("explicitNull", null);
80
+
81
+ ArrayList<Bundle> result =
82
+ ParcelableCompatReader.getParcelableArrayList(bundle, "bundles", Bundle.class);
83
+
84
+ assertNotNull(result);
85
+ assertEquals(1, result.size());
86
+ assertEquals("first", result.get(0).getString("name"));
87
+ assertNull(ParcelableCompatReader.getParcelableArrayList(bundle, "missing", Bundle.class));
88
+ assertNull(ParcelableCompatReader.getParcelableArrayList(bundle, "explicitNull", Bundle.class));
89
+ }
90
+
91
+ @Test
92
+ @Config(sdk = 33)
93
+ public void getParcelableArrayList_typedPath_returnsBundleList() {
94
+ Bundle bundle = new Bundle();
95
+ ArrayList<Bundle> bundles = createBundleList();
96
+ bundle.putParcelableArrayList("bundles", bundles);
97
+ bundle.putParcelableArrayList("explicitNull", null);
98
+
99
+ ArrayList<Bundle> result =
100
+ ParcelableCompatReader.getParcelableArrayList(bundle, "bundles", Bundle.class);
101
+
102
+ assertNotNull(result);
103
+ assertEquals(1, result.size());
104
+ assertEquals("first", result.get(0).getString("name"));
105
+ assertNull(ParcelableCompatReader.getParcelableArrayList(bundle, "missing", Bundle.class));
106
+ assertNull(ParcelableCompatReader.getParcelableArrayList(bundle, "explicitNull", Bundle.class));
107
+ }
108
+
109
+ private static Notification createNotification() {
110
+ return new Notification.Builder(RuntimeEnvironment.getApplication(), "reader-test")
111
+ .setSmallIcon(android.R.drawable.ic_dialog_info)
112
+ .setContentTitle("reader-test")
113
+ .build();
114
+ }
115
+
116
+ private static ArrayList<Bundle> createBundleList() {
117
+ Bundle nested = new Bundle();
118
+ nested.putString("name", "first");
119
+
120
+ ArrayList<Bundle> bundles = new ArrayList<>();
121
+ bundles.add(nested);
122
+ return bundles;
123
+ }
124
+ }
@@ -0,0 +1,136 @@
1
+ package io.invertase.notifee;
2
+
3
+ import static org.junit.Assert.assertEquals;
4
+ import static org.mockito.ArgumentMatchers.any;
5
+ import static org.mockito.ArgumentMatchers.anyInt;
6
+ import static org.mockito.ArgumentMatchers.eq;
7
+ import static org.mockito.ArgumentMatchers.same;
8
+ import static org.mockito.Mockito.mock;
9
+ import static org.mockito.Mockito.never;
10
+ import static org.mockito.Mockito.verify;
11
+ import static org.mockito.Mockito.when;
12
+
13
+ import android.app.Application;
14
+ import android.os.Looper;
15
+ import app.notifee.core.ContextHolder;
16
+ import com.facebook.react.ReactInstanceEventListener;
17
+ import com.facebook.react.bridge.JavaOnlyMap;
18
+ import com.facebook.react.bridge.ReactContext;
19
+ import com.facebook.react.bridge.WritableMap;
20
+ import com.facebook.react.common.LifecycleState;
21
+ import com.facebook.react.modules.appregistry.AppRegistry;
22
+ import java.util.ArrayList;
23
+ import java.util.List;
24
+ import org.junit.Before;
25
+ import org.junit.Test;
26
+ import org.junit.runner.RunWith;
27
+ import org.mockito.ArgumentCaptor;
28
+ import org.robolectric.RuntimeEnvironment;
29
+ import org.robolectric.Shadows;
30
+ import org.robolectric.RobolectricTestRunner;
31
+ import org.robolectric.annotation.Config;
32
+ import org.robolectric.annotation.LooperMode;
33
+
34
+ @RunWith(RobolectricTestRunner.class)
35
+ @Config(application = HeadlessTaskReactHostTest.TestApplication.class, sdk = 34)
36
+ @LooperMode(LooperMode.Mode.PAUSED)
37
+ public class HeadlessTaskReactHostTest {
38
+
39
+ private TestApplication application;
40
+
41
+ @Before
42
+ public void setUp() {
43
+ application = (TestApplication) RuntimeEnvironment.getApplication();
44
+ application.reactHost.reset();
45
+ ContextHolder.setApplicationContext(application);
46
+ Shadows.shadowOf(Looper.getMainLooper()).idle();
47
+ }
48
+
49
+ @Test
50
+ public void startTask_whenReactContextInitializes_drainsQueuedTaskAfterCurrentDelay() {
51
+ JavaOnlyMap params = new JavaOnlyMap();
52
+ params.putString("source", "react-host-test");
53
+
54
+ HeadlessTask headlessTask = new HeadlessTask();
55
+ HeadlessTask.TaskConfig taskConfig =
56
+ new HeadlessTask.TaskConfig("test-headless-task", 60000L, params, null);
57
+ ReactContext reactContext = mock(ReactContext.class);
58
+ AppRegistry appRegistry = mock(AppRegistry.class);
59
+ when(reactContext.getLifecycleState()).thenReturn(LifecycleState.BEFORE_RESUME);
60
+ when(reactContext.hasActiveReactInstance()).thenReturn(true);
61
+ when(reactContext.getJSModule(AppRegistry.class)).thenReturn(appRegistry);
62
+
63
+ WritableMap copiedParams = taskConfig.getTaskConfig().getData();
64
+
65
+ headlessTask.startTask(application, taskConfig);
66
+
67
+ assertEquals(
68
+ "ReactHost should be started through reflection", 1, application.reactHost.startCalls);
69
+ assertEquals(
70
+ "ReactContext listener should be registered through reflection",
71
+ 1,
72
+ application.reactHost.listeners.size());
73
+ verify(appRegistry, never())
74
+ .startHeadlessTask(anyInt(), any(String.class), any(WritableMap.class));
75
+
76
+ ReactInstanceEventListener listener = application.reactHost.listeners.get(0);
77
+ listener.onReactContextInitialized(reactContext);
78
+
79
+ assertEquals(
80
+ "ReactContext listener should be removed after initialization",
81
+ 0,
82
+ application.reactHost.listeners.size());
83
+ Shadows.shadowOf(Looper.getMainLooper()).idleFor(499, java.util.concurrent.TimeUnit.MILLISECONDS);
84
+ verify(appRegistry, never()).startHeadlessTask(anyInt(), any(String.class), any(WritableMap.class));
85
+
86
+ Shadows.shadowOf(Looper.getMainLooper()).idleFor(1, java.util.concurrent.TimeUnit.MILLISECONDS);
87
+
88
+ ArgumentCaptor<Integer> taskIdCaptor = ArgumentCaptor.forClass(Integer.class);
89
+ verify(appRegistry)
90
+ .startHeadlessTask(
91
+ taskIdCaptor.capture(), eq("test-headless-task"), same(copiedParams));
92
+ assertEquals(taskIdCaptor.getValue().intValue(), taskConfig.getReactTaskId());
93
+ assertEquals(
94
+ "source payload should be copied into the queued task",
95
+ "react-host-test",
96
+ copiedParams.getString("source"));
97
+ assertEquals(
98
+ "copied params should receive the native task id",
99
+ taskConfig.getTaskId(),
100
+ copiedParams.getInt("taskId"));
101
+ }
102
+
103
+ public static class TestApplication extends Application {
104
+ final TestReactHost reactHost = new TestReactHost();
105
+
106
+ public TestReactHost getReactHost() {
107
+ return reactHost;
108
+ }
109
+ }
110
+
111
+ public static class TestReactHost {
112
+ int startCalls;
113
+ final List<ReactInstanceEventListener> listeners = new ArrayList<>();
114
+
115
+ public ReactContext getCurrentReactContext() {
116
+ return null;
117
+ }
118
+
119
+ public void addReactInstanceEventListener(ReactInstanceEventListener listener) {
120
+ listeners.add(listener);
121
+ }
122
+
123
+ public void removeReactInstanceEventListener(ReactInstanceEventListener listener) {
124
+ listeners.remove(listener);
125
+ }
126
+
127
+ public void start() {
128
+ startCalls++;
129
+ }
130
+
131
+ void reset() {
132
+ startCalls = 0;
133
+ listeners.clear();
134
+ }
135
+ }
136
+ }
@@ -0,0 +1,262 @@
1
+ package io.invertase.notifee;
2
+
3
+ import static org.junit.Assert.assertEquals;
4
+ import static org.junit.Assert.assertSame;
5
+ import static org.mockito.ArgumentMatchers.any;
6
+ import static org.mockito.Mockito.mock;
7
+ import static org.mockito.Mockito.mockStatic;
8
+ import static org.mockito.Mockito.never;
9
+ import static org.mockito.Mockito.verify;
10
+ import static org.mockito.Mockito.when;
11
+
12
+ import android.app.Application;
13
+ import android.os.Bundle;
14
+ import android.os.Looper;
15
+ import androidx.lifecycle.ProcessLifecycleOwner;
16
+ import app.notifee.core.ContextHolder;
17
+ import app.notifee.core.event.NotificationEvent;
18
+ import app.notifee.core.model.NotificationModel;
19
+ import com.facebook.react.ReactInstanceEventListener;
20
+ import com.facebook.react.bridge.Arguments;
21
+ import com.facebook.react.bridge.ReactContext;
22
+ import com.facebook.react.bridge.WritableMap;
23
+ import com.facebook.react.modules.core.DeviceEventManagerModule;
24
+ import java.lang.reflect.Field;
25
+ import java.util.ArrayList;
26
+ import java.util.List;
27
+ import java.util.concurrent.atomic.AtomicBoolean;
28
+ import org.junit.After;
29
+ import org.junit.Before;
30
+ import org.junit.Test;
31
+ import org.junit.runner.RunWith;
32
+ import org.mockito.MockedStatic;
33
+ import org.robolectric.RuntimeEnvironment;
34
+ import org.robolectric.Shadows;
35
+ import org.robolectric.RobolectricTestRunner;
36
+ import org.robolectric.annotation.Config;
37
+
38
+ @RunWith(RobolectricTestRunner.class)
39
+ @Config(application = NotifeeEventSubscriberRoutingTest.TestApplication.class, sdk = 34)
40
+ public class NotifeeEventSubscriberRoutingTest {
41
+
42
+ private TestApplication application;
43
+ private HeadlessTask headlessTask;
44
+
45
+ @Before
46
+ public void setUp() throws Exception {
47
+ application = (TestApplication) RuntimeEnvironment.getApplication();
48
+ application.reactHost.reset();
49
+ ContextHolder.setApplicationContext(application);
50
+ headlessTask = NotifeeReactUtils.INSTANCE.getHeadlessTaskManager();
51
+ resetHeadlessTask(headlessTask);
52
+ moveProcessToBackground();
53
+ Shadows.shadowOf(Looper.getMainLooper()).idle();
54
+ }
55
+
56
+ @After
57
+ public void tearDown() throws Exception {
58
+ moveProcessToBackground();
59
+ resetHeadlessTask(headlessTask);
60
+ application.reactHost.reset();
61
+ }
62
+
63
+ @Test
64
+ public void onNotificationEvent_typePressWhenResumed_routesToForegroundEvent() throws Exception {
65
+ moveProcessToForeground();
66
+ DeviceEventManagerModule.RCTDeviceEventEmitter emitter =
67
+ mock(DeviceEventManagerModule.RCTDeviceEventEmitter.class);
68
+ ReactContext reactContext = mock(ReactContext.class);
69
+ when(reactContext.hasActiveReactInstance()).thenReturn(true);
70
+ when(reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class))
71
+ .thenReturn(emitter);
72
+ application.reactHost.currentReactContext = reactContext;
73
+
74
+ WritableMap eventMap = mock(WritableMap.class);
75
+ WritableMap detailMap = mock(WritableMap.class);
76
+ WritableMap notificationMap = mock(WritableMap.class);
77
+ WritableMap pressActionMap = mock(WritableMap.class);
78
+
79
+ try (MockedStatic<Arguments> arguments = mockArguments(
80
+ eventMap, detailMap, notificationMap, pressActionMap)) {
81
+ new NotifeeEventSubscriber().onNotificationEvent(buildPressEvent("routing-foreground"));
82
+ }
83
+
84
+ verify(eventMap).putInt("type", NotificationEvent.TYPE_PRESS);
85
+ verify(detailMap).putMap("notification", notificationMap);
86
+ verify(detailMap).putMap("pressAction", pressActionMap);
87
+ verify(detailMap).putString("input", "typed reply");
88
+ verify(eventMap).putMap("detail", detailMap);
89
+ verify(eventMap).putBoolean("headless", false);
90
+ verify(emitter).emit(NotifeeEventSubscriber.NOTIFICATION_EVENT_KEY, eventMap);
91
+ verify(eventMap, never()).copy();
92
+ assertEquals("foreground routing must not queue a headless task", 0, taskQueueSize());
93
+ assertEquals("foreground routing must not start ReactHost", 0, application.reactHost.startCalls);
94
+ }
95
+
96
+ @Test
97
+ public void onNotificationEvent_typePressWhenNotResumed_routesToHeadlessTask() throws Exception {
98
+ moveProcessToBackground();
99
+ DeviceEventManagerModule.RCTDeviceEventEmitter emitter =
100
+ mock(DeviceEventManagerModule.RCTDeviceEventEmitter.class);
101
+ ReactContext reactContext = mock(ReactContext.class);
102
+ when(reactContext.hasActiveReactInstance()).thenReturn(true);
103
+ when(reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class))
104
+ .thenReturn(emitter);
105
+ application.reactHost.currentReactContext = null;
106
+
107
+ WritableMap eventMap = mock(WritableMap.class);
108
+ WritableMap queuedEventMap = mock(WritableMap.class);
109
+ WritableMap detailMap = mock(WritableMap.class);
110
+ WritableMap notificationMap = mock(WritableMap.class);
111
+ WritableMap pressActionMap = mock(WritableMap.class);
112
+ when(eventMap.copy()).thenReturn(queuedEventMap);
113
+
114
+ try (MockedStatic<Arguments> arguments = mockArguments(
115
+ eventMap, detailMap, notificationMap, pressActionMap)) {
116
+ new NotifeeEventSubscriber().onNotificationEvent(buildPressEvent("routing-background"));
117
+ }
118
+
119
+ verify(eventMap).putInt("type", NotificationEvent.TYPE_PRESS);
120
+ verify(detailMap).putMap("notification", notificationMap);
121
+ verify(detailMap).putMap("pressAction", pressActionMap);
122
+ verify(detailMap).putString("input", "typed reply");
123
+ verify(eventMap).putMap("detail", detailMap);
124
+ verify(eventMap).putBoolean("headless", true);
125
+ verify(eventMap).copy();
126
+ verify(queuedEventMap).putInt("taskId", headlessTaskQueue().get(0).getTaskId());
127
+ verify(emitter, never()).emit(any(String.class), any(WritableMap.class));
128
+
129
+ assertEquals("background routing should queue exactly one headless task", 1, taskQueueSize());
130
+ HeadlessTask.TaskConfig queuedTask = headlessTaskQueue().get(0);
131
+ assertEquals(NotifeeEventSubscriber.NOTIFICATION_EVENT_KEY, queuedTask.getTaskConfig().getTaskKey());
132
+ assertEquals(60000L, queuedTask.getTaskConfig().getTimeout());
133
+ assertSame(queuedEventMap, queuedTask.getTaskConfig().getData());
134
+ assertEquals("background routing should initialize ReactHost via reflection", 1, application.reactHost.startCalls);
135
+ assertEquals("background routing should register one ReactContext listener", 1, application.reactHost.listeners.size());
136
+ }
137
+
138
+ private static MockedStatic<Arguments> mockArguments(
139
+ WritableMap eventMap,
140
+ WritableMap detailMap,
141
+ WritableMap notificationMap,
142
+ WritableMap pressActionMap) {
143
+ MockedStatic<Arguments> arguments = mockStatic(Arguments.class);
144
+ arguments.when(Arguments::createMap).thenReturn(eventMap, detailMap);
145
+ arguments.when(() -> Arguments.fromBundle(any(Bundle.class)))
146
+ .thenReturn(notificationMap, pressActionMap);
147
+ return arguments;
148
+ }
149
+
150
+ private static NotificationEvent buildPressEvent(String notificationId) {
151
+ Bundle notificationBundle = new Bundle();
152
+ notificationBundle.putString("id", notificationId);
153
+ notificationBundle.putString("title", "Routing test " + notificationId);
154
+ Bundle androidBundle = new Bundle();
155
+ androidBundle.putString("channelId", "routing-test-channel");
156
+ notificationBundle.putBundle("android", androidBundle);
157
+
158
+ Bundle pressActionBundle = new Bundle();
159
+ pressActionBundle.putString("id", "default");
160
+ pressActionBundle.putString("launchActivity", "default");
161
+
162
+ Bundle extras = new Bundle();
163
+ extras.putBundle("pressAction", pressActionBundle);
164
+ extras.putString("input", "typed reply");
165
+
166
+ return new NotificationEvent(
167
+ NotificationEvent.TYPE_PRESS, NotificationModel.fromBundle(notificationBundle), extras);
168
+ }
169
+
170
+ private static void moveProcessToForeground() {
171
+ ProcessLifecycleOwner owner = (ProcessLifecycleOwner) ProcessLifecycleOwner.get();
172
+ owner.activityStarted$lifecycle_process_release();
173
+ owner.activityResumed$lifecycle_process_release();
174
+ }
175
+
176
+ private static void moveProcessToBackground() throws Exception {
177
+ ProcessLifecycleOwner.init$lifecycle_process_release(RuntimeEnvironment.getApplication());
178
+ ProcessLifecycleOwner owner = (ProcessLifecycleOwner) ProcessLifecycleOwner.get();
179
+ int resumedCounter = getIntField(owner, "resumedCounter");
180
+ for (int i = 0; i < resumedCounter; i++) {
181
+ owner.activityPaused$lifecycle_process_release();
182
+ }
183
+ owner.dispatchPauseIfNeeded$lifecycle_process_release();
184
+
185
+ int startedCounter = getIntField(owner, "startedCounter");
186
+ for (int i = 0; i < startedCounter; i++) {
187
+ owner.activityStopped$lifecycle_process_release();
188
+ }
189
+ owner.dispatchStopIfNeeded$lifecycle_process_release();
190
+ }
191
+
192
+ private static int getIntField(Object target, String name) throws Exception {
193
+ Field field = target.getClass().getDeclaredField(name);
194
+ field.setAccessible(true);
195
+ return field.getInt(target);
196
+ }
197
+
198
+ private int taskQueueSize() throws Exception {
199
+ return headlessTaskQueue().size();
200
+ }
201
+
202
+ @SuppressWarnings("unchecked")
203
+ private List<HeadlessTask.TaskConfig> headlessTaskQueue() throws Exception {
204
+ Field queueField = HeadlessTask.class.getDeclaredField("mTaskQueue");
205
+ queueField.setAccessible(true);
206
+ return (List<HeadlessTask.TaskConfig>) queueField.get(headlessTask);
207
+ }
208
+
209
+ private static void resetHeadlessTask(HeadlessTask task) throws Exception {
210
+ Field queueField = HeadlessTask.class.getDeclaredField("mTaskQueue");
211
+ queueField.setAccessible(true);
212
+ ((List<?>) queueField.get(task)).clear();
213
+
214
+ setAtomicBoolean(task, "mIsReactContextInitialized", false);
215
+ setAtomicBoolean(task, "mWillDrainTaskQueue", false);
216
+ setAtomicBoolean(task, "mIsInitializingReactContext", false);
217
+ setAtomicBoolean(task, "mIsHeadlessJsTaskListenerRegistered", false);
218
+ }
219
+
220
+ private static void setAtomicBoolean(HeadlessTask task, String name, boolean value)
221
+ throws Exception {
222
+ Field field = HeadlessTask.class.getDeclaredField(name);
223
+ field.setAccessible(true);
224
+ ((AtomicBoolean) field.get(task)).set(value);
225
+ }
226
+
227
+ public static class TestApplication extends Application {
228
+ final TestReactHost reactHost = new TestReactHost();
229
+
230
+ public TestReactHost getReactHost() {
231
+ return reactHost;
232
+ }
233
+ }
234
+
235
+ public static class TestReactHost {
236
+ ReactContext currentReactContext;
237
+ int startCalls;
238
+ final List<ReactInstanceEventListener> listeners = new ArrayList<>();
239
+
240
+ public ReactContext getCurrentReactContext() {
241
+ return currentReactContext;
242
+ }
243
+
244
+ public void addReactInstanceEventListener(ReactInstanceEventListener listener) {
245
+ listeners.add(listener);
246
+ }
247
+
248
+ public void removeReactInstanceEventListener(ReactInstanceEventListener listener) {
249
+ listeners.remove(listener);
250
+ }
251
+
252
+ public void start() {
253
+ startCalls++;
254
+ }
255
+
256
+ void reset() {
257
+ currentReactContext = null;
258
+ startCalls = 0;
259
+ listeners.clear();
260
+ }
261
+ }
262
+ }
@@ -0,0 +1,24 @@
1
+ package io.invertase.notifee;
2
+
3
+ import app.notifee.core.ContextHolder;
4
+ import org.junit.Before;
5
+ import org.junit.Test;
6
+ import org.junit.runner.RunWith;
7
+ import org.robolectric.RuntimeEnvironment;
8
+ import org.robolectric.RobolectricTestRunner;
9
+ import org.robolectric.annotation.Config;
10
+
11
+ @RunWith(RobolectricTestRunner.class)
12
+ @Config(sdk = 34)
13
+ public class NotifeeReactUtilsDrawerTest {
14
+
15
+ @Before
16
+ public void setUp() {
17
+ ContextHolder.setApplicationContext(RuntimeEnvironment.getApplication());
18
+ }
19
+
20
+ @Test
21
+ public void hideNotificationDrawer_whenStatusBarReflectionFails_doesNotCrash() {
22
+ NotifeeReactUtils.INSTANCE.hideNotificationDrawer();
23
+ }
24
+ }
@@ -2,7 +2,9 @@
2
2
  * Patches the Podfile to add the NSE target with RNNotifeeCore pod.
3
3
  * The NSE target is nested inside the main app target so CocoaPods can
4
4
  * detect the host→extension relationship. Uses `inherit! :search_paths`.
5
- * Idempotent: if the target already exists, returns false.
5
+ * Also installs a post_install hook that keeps React Native Firebase's
6
+ * generated Info.plist input path from recreating a host-extension build cycle.
7
+ * Idempotent: returns false when no Podfile changes are needed.
6
8
  */
7
9
  export declare function patchPodfile(podfilePath: string, targetName: string, dryRun: boolean): boolean;
8
10
  /**