expo-notifications 0.28.13 → 0.28.15
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/CHANGELOG.md +12 -0
- package/android/build.gradle +2 -2
- package/android/src/main/java/expo/modules/notifications/Utils.kt +89 -0
- package/android/src/main/java/expo/modules/notifications/notifications/NotificationSerializer.java +9 -21
- package/build/useLastNotificationResponse.d.ts +3 -1
- package/build/useLastNotificationResponse.d.ts.map +1 -1
- package/build/useLastNotificationResponse.js +23 -16
- package/build/useLastNotificationResponse.js.map +1 -1
- package/ios/EXNotifications/Notifications/EXNotificationCenterDelegate.h +4 -0
- package/ios/EXNotifications/Notifications/EXNotificationCenterDelegate.m +2 -0
- package/ios/EXNotifications/Notifications/Emitter/EXNotificationsEmitter.m +3 -4
- package/package.json +2 -2
- package/src/useLastNotificationResponse.ts +39 -20
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,18 @@
|
|
|
10
10
|
|
|
11
11
|
### 💡 Others
|
|
12
12
|
|
|
13
|
+
## 0.28.15 — 2024-08-05
|
|
14
|
+
|
|
15
|
+
### 🐛 Bug fixes
|
|
16
|
+
|
|
17
|
+
- [Android] Eliminate unsupported types when processing notification intents from onCreate/onNewIntent. ([#30750](https://github.com/expo/expo/pull/30750) by [@douglowder](https://github.com/douglowder))
|
|
18
|
+
|
|
19
|
+
## 0.28.14 — 2024-07-30
|
|
20
|
+
|
|
21
|
+
### 🐛 Bug fixes
|
|
22
|
+
|
|
23
|
+
- `useLastNotificationResponse` should have only one effect. ([#30653](https://github.com/expo/expo/pull/30653) by [@douglowder](https://github.com/douglowder))
|
|
24
|
+
|
|
13
25
|
## 0.28.13 — 2024-07-29
|
|
14
26
|
|
|
15
27
|
### 🐛 Bug fixes
|
package/android/build.gradle
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
apply plugin: 'com.android.library'
|
|
2
2
|
|
|
3
3
|
group = 'host.exp.exponent'
|
|
4
|
-
version = '0.28.
|
|
4
|
+
version = '0.28.15'
|
|
5
5
|
|
|
6
6
|
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
|
7
7
|
apply from: expoModulesCorePlugin
|
|
@@ -14,7 +14,7 @@ android {
|
|
|
14
14
|
namespace "expo.modules.notifications"
|
|
15
15
|
defaultConfig {
|
|
16
16
|
versionCode 21
|
|
17
|
-
versionName '0.28.
|
|
17
|
+
versionName '0.28.15'
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
buildFeatures {
|
|
@@ -3,8 +3,13 @@ package expo.modules.notifications
|
|
|
3
3
|
import android.os.Bundle
|
|
4
4
|
import android.os.Handler
|
|
5
5
|
import android.os.ResultReceiver
|
|
6
|
+
import expo.modules.kotlin.types.JSTypeConverter
|
|
7
|
+
import org.json.JSONArray
|
|
8
|
+
import org.json.JSONException
|
|
9
|
+
import org.json.JSONObject
|
|
6
10
|
|
|
7
11
|
typealias ResultReceiverBody = (resultCode: Int, resultData: Bundle?) -> Unit
|
|
12
|
+
typealias BundleConversionTester = (bundle: Bundle) -> Boolean
|
|
8
13
|
|
|
9
14
|
internal fun createDefaultResultReceiver(
|
|
10
15
|
handler: Handler?,
|
|
@@ -17,3 +22,87 @@ internal fun createDefaultResultReceiver(
|
|
|
17
22
|
}
|
|
18
23
|
}
|
|
19
24
|
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Given an input bundle, creates a new bundle with non-convertible objects removed
|
|
28
|
+
*/
|
|
29
|
+
internal fun filteredBundleForJSTypeConverter(bundle: Bundle): Bundle {
|
|
30
|
+
return filteredBundleForJSTypeConverter(bundle, isBundleConvertibleToJSValue)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
internal fun filteredBundleForJSTypeConverter(bundle: Bundle, testBundle: BundleConversionTester): Bundle {
|
|
34
|
+
return when (testBundle(bundle)) {
|
|
35
|
+
true -> bundle
|
|
36
|
+
else -> {
|
|
37
|
+
// Store keys whose values are convertible
|
|
38
|
+
val goodKeys: MutableSet<String> = mutableSetOf()
|
|
39
|
+
// Do first pass to filter any values that are bundles
|
|
40
|
+
bundle.keySet().forEach { key: String ->
|
|
41
|
+
val value = bundle[key]
|
|
42
|
+
if (value is Bundle) {
|
|
43
|
+
bundle.putBundle(key, filteredBundleForJSTypeConverter(value, testBundle))
|
|
44
|
+
goodKeys.add(key)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Second pass: create a bundle with just the value for that key, and see if it converts
|
|
48
|
+
// There is no generic put() method for bundles, so we putAll() and then remove values
|
|
49
|
+
// other than the one we are testing
|
|
50
|
+
bundle.keySet().forEach { key: String ->
|
|
51
|
+
if (!goodKeys.contains(key)) {
|
|
52
|
+
val test = Bundle()
|
|
53
|
+
test.putAll(bundle)
|
|
54
|
+
bundle.keySet().forEach { otherKey: String ->
|
|
55
|
+
if (!otherKey.equals(key)) {
|
|
56
|
+
test.remove(otherKey)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (testBundle(test)) {
|
|
60
|
+
goodKeys.add(key)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Now create a new bundle, remove keys that are not good, and return
|
|
65
|
+
val result = Bundle()
|
|
66
|
+
result.putAll(bundle)
|
|
67
|
+
bundle.keySet().forEach { key: String ->
|
|
68
|
+
if (!goodKeys.contains(key)) {
|
|
69
|
+
result.remove(key)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
result
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
internal val isBundleConvertibleToJSValue: BundleConversionTester = { bundle: Bundle ->
|
|
78
|
+
try {
|
|
79
|
+
JSTypeConverter.convertToJSValue(bundle)
|
|
80
|
+
true
|
|
81
|
+
} catch (e: Throwable) {
|
|
82
|
+
false
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Returns true if the argument is a valid JSON string, false otherwise
|
|
88
|
+
*/
|
|
89
|
+
internal fun isValidJSONString(test: Any?): Boolean {
|
|
90
|
+
when (test is String) {
|
|
91
|
+
true -> {
|
|
92
|
+
try {
|
|
93
|
+
JSONObject(test as String)
|
|
94
|
+
return true
|
|
95
|
+
} catch (objectEx: JSONException) {
|
|
96
|
+
try {
|
|
97
|
+
JSONArray(test as String)
|
|
98
|
+
return true
|
|
99
|
+
} catch (arrayEx: JSONException) {
|
|
100
|
+
return false
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else -> {
|
|
105
|
+
return false
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
package/android/src/main/java/expo/modules/notifications/notifications/NotificationSerializer.java
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
package expo.modules.notifications.notifications;
|
|
2
2
|
|
|
3
|
+
import static expo.modules.notifications.UtilsKt.filteredBundleForJSTypeConverter;
|
|
4
|
+
import static expo.modules.notifications.UtilsKt.isValidJSONString;
|
|
5
|
+
|
|
3
6
|
import android.os.Build;
|
|
4
7
|
import android.os.Bundle;
|
|
5
8
|
|
|
@@ -9,7 +12,6 @@ import com.google.firebase.messaging.RemoteMessage;
|
|
|
9
12
|
|
|
10
13
|
import org.jetbrains.annotations.NotNull;
|
|
11
14
|
import org.json.JSONArray;
|
|
12
|
-
import org.json.JSONException;
|
|
13
15
|
import org.json.JSONObject;
|
|
14
16
|
import expo.modules.core.arguments.MapArguments;
|
|
15
17
|
|
|
@@ -58,8 +60,7 @@ public class NotificationSerializer {
|
|
|
58
60
|
serializedRequest.putBundle("trigger", toBundle(request.getTrigger()));
|
|
59
61
|
Bundle content = toBundle(request.getContent());
|
|
60
62
|
Bundle existingContentData = content.getBundle("data");
|
|
61
|
-
if (existingContentData == null) {
|
|
62
|
-
FirebaseNotificationTrigger trigger = (FirebaseNotificationTrigger) request.getTrigger();
|
|
63
|
+
if (existingContentData == null && request.getTrigger() instanceof FirebaseNotificationTrigger trigger) {
|
|
63
64
|
RemoteMessage message = trigger.getRemoteMessage();
|
|
64
65
|
RemoteMessage.Notification notification = message.getNotification();
|
|
65
66
|
Map<String, String> data = message.getData();
|
|
@@ -226,17 +227,17 @@ public class NotificationSerializer {
|
|
|
226
227
|
Bundle serializedContent = new Bundle();
|
|
227
228
|
serializedContent.putString("title", extras.getString("title"));
|
|
228
229
|
String body = extras.getString("body");
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
// If projectId is set in the bundle, and the body is a JSON string,
|
|
230
|
+
if (isValidJSONString(body) ) {
|
|
231
|
+
// If the body is a JSON string,
|
|
232
232
|
// the notification was sent by the Expo notification service,
|
|
233
233
|
// so we do the expected remapping of fields
|
|
234
234
|
serializedContent.putString("dataString", body);
|
|
235
235
|
serializedContent.putString("body", extras.getString("message"));
|
|
236
236
|
} else {
|
|
237
237
|
// The notification came directly from Firebase or some other service,
|
|
238
|
-
// so we copy the data as is from the extras bundle
|
|
239
|
-
|
|
238
|
+
// so we copy the data as is from the extras bundle, after
|
|
239
|
+
// ensuring it can be converted for emitting to JS
|
|
240
|
+
serializedContent.putBundle("data", filteredBundleForJSTypeConverter(extras));
|
|
240
241
|
}
|
|
241
242
|
|
|
242
243
|
Bundle serializedTrigger = new Bundle();
|
|
@@ -259,17 +260,4 @@ public class NotificationSerializer {
|
|
|
259
260
|
return serializedResponse;
|
|
260
261
|
}
|
|
261
262
|
|
|
262
|
-
public static boolean isValidJSONString(String test) {
|
|
263
|
-
try {
|
|
264
|
-
new JSONObject(test);
|
|
265
|
-
} catch (JSONException objectEx) {
|
|
266
|
-
try {
|
|
267
|
-
new JSONArray(test);
|
|
268
|
-
} catch (JSONException arrayEx) {
|
|
269
|
-
return false;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
return true;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
263
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { NotificationResponse } from './Notifications.types';
|
|
2
|
+
type MaybeNotificationResponse = NotificationResponse | null | undefined;
|
|
2
3
|
/**
|
|
3
4
|
* A React hook always returns the notification response that was received most recently
|
|
4
5
|
* (a notification response designates an interaction with a notification, such as tapping on it).
|
|
@@ -35,5 +36,6 @@ import { NotificationResponse } from './Notifications.types';
|
|
|
35
36
|
* ```
|
|
36
37
|
* @header listen
|
|
37
38
|
*/
|
|
38
|
-
export default function useLastNotificationResponse():
|
|
39
|
+
export default function useLastNotificationResponse(): MaybeNotificationResponse;
|
|
40
|
+
export {};
|
|
39
41
|
//# sourceMappingURL=useLastNotificationResponse.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useLastNotificationResponse.d.ts","sourceRoot":"","sources":["../src/useLastNotificationResponse.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"useLastNotificationResponse.d.ts","sourceRoot":"","sources":["../src/useLastNotificationResponse.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAM7D,KAAK,yBAAyB,GAAG,oBAAoB,GAAG,IAAI,GAAG,SAAS,CAAC;AAEzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,CAAC,OAAO,UAAU,2BAA2B,8BA0ClD"}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { addNotificationResponseReceivedListener } from './NotificationsEmitter';
|
|
3
|
-
import NotificationsEmitterModule from './NotificationsEmitterModule';
|
|
4
|
-
import { mapNotificationResponse } from './utils/mapNotificationResponse';
|
|
1
|
+
import { useLayoutEffect, useState } from 'react';
|
|
2
|
+
import { addNotificationResponseReceivedListener, getLastNotificationResponseAsync, } from './NotificationsEmitter';
|
|
5
3
|
/**
|
|
6
4
|
* A React hook always returns the notification response that was received most recently
|
|
7
5
|
* (a notification response designates an interaction with a notification, such as tapping on it).
|
|
@@ -40,24 +38,33 @@ import { mapNotificationResponse } from './utils/mapNotificationResponse';
|
|
|
40
38
|
*/
|
|
41
39
|
export default function useLastNotificationResponse() {
|
|
42
40
|
const [lastNotificationResponse, setLastNotificationResponse] = useState(undefined);
|
|
41
|
+
// Pure function that returns the new response if it is different from the previous,
|
|
42
|
+
// otherwise return the previous response
|
|
43
|
+
const newResponseIfNeeded = (prevResponse, newResponse) => {
|
|
44
|
+
// If the new response is undefined or null, no need for update
|
|
45
|
+
if (!newResponse) {
|
|
46
|
+
return prevResponse;
|
|
47
|
+
}
|
|
48
|
+
// If the previous response is undefined or null and the new response is not, we should update
|
|
49
|
+
if (!prevResponse) {
|
|
50
|
+
return newResponse;
|
|
51
|
+
}
|
|
52
|
+
return prevResponse.notification.request.identifier !==
|
|
53
|
+
newResponse.notification.request.identifier
|
|
54
|
+
? newResponse
|
|
55
|
+
: prevResponse;
|
|
56
|
+
};
|
|
43
57
|
// useLayoutEffect ensures the listener is registered as soon as possible
|
|
44
58
|
useLayoutEffect(() => {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
59
|
+
// Get the last response first, in case it was set earlier (even in native code on startup)
|
|
60
|
+
// before this renders
|
|
61
|
+
getLastNotificationResponseAsync?.().then((response) => setLastNotificationResponse((prevResponse) => newResponseIfNeeded(prevResponse, response)));
|
|
62
|
+
// Set up listener for responses that come in, and set the last response if needed
|
|
63
|
+
const subscription = addNotificationResponseReceivedListener((response) => setLastNotificationResponse((prevResponse) => newResponseIfNeeded(prevResponse, response)));
|
|
49
64
|
return () => {
|
|
50
65
|
subscription.remove();
|
|
51
66
|
};
|
|
52
67
|
}, []);
|
|
53
|
-
// On each mount of this hook we fetch last notification response
|
|
54
|
-
// from the native module which is an "always active listener"
|
|
55
|
-
// and always returns the most recent response.
|
|
56
|
-
useEffect(() => {
|
|
57
|
-
NotificationsEmitterModule.getLastNotificationResponseAsync?.().then((response) => {
|
|
58
|
-
setLastNotificationResponse(response);
|
|
59
|
-
});
|
|
60
|
-
}, []);
|
|
61
68
|
return lastNotificationResponse;
|
|
62
69
|
}
|
|
63
70
|
//# sourceMappingURL=useLastNotificationResponse.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useLastNotificationResponse.js","sourceRoot":"","sources":["../src/useLastNotificationResponse.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"useLastNotificationResponse.js","sourceRoot":"","sources":["../src/useLastNotificationResponse.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAGlD,OAAO,EACL,uCAAuC,EACvC,gCAAgC,GACjC,MAAM,wBAAwB,CAAC;AAIhC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,CAAC,OAAO,UAAU,2BAA2B;IACjD,MAAM,CAAC,wBAAwB,EAAE,2BAA2B,CAAC,GAC3D,QAAQ,CAA4B,SAAS,CAAC,CAAC;IAEjD,oFAAoF;IACpF,yCAAyC;IACzC,MAAM,mBAAmB,GAAG,CAC1B,YAAuC,EACvC,WAAsC,EACtC,EAAE;QACF,+DAA+D;QAC/D,IAAI,CAAC,WAAW,EAAE;YAChB,OAAO,YAAY,CAAC;SACrB;QACD,8FAA8F;QAC9F,IAAI,CAAC,YAAY,EAAE;YACjB,OAAO,WAAW,CAAC;SACpB;QACD,OAAO,YAAY,CAAC,YAAY,CAAC,OAAO,CAAC,UAAU;YACjD,WAAW,CAAC,YAAY,CAAC,OAAO,CAAC,UAAU;YAC3C,CAAC,CAAC,WAAW;YACb,CAAC,CAAC,YAAY,CAAC;IACnB,CAAC,CAAC;IAEF,yEAAyE;IACzE,eAAe,CAAC,GAAG,EAAE;QACnB,2FAA2F;QAC3F,sBAAsB;QACtB,gCAAgC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE,CACrD,2BAA2B,CAAC,CAAC,YAAY,EAAE,EAAE,CAAC,mBAAmB,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC,CAC3F,CAAC;QAEF,kFAAkF;QAClF,MAAM,YAAY,GAAG,uCAAuC,CAAC,CAAC,QAAQ,EAAE,EAAE,CACxE,2BAA2B,CAAC,CAAC,YAAY,EAAE,EAAE,CAAC,mBAAmB,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC,CAC3F,CAAC;QACF,OAAO,GAAG,EAAE;YACV,YAAY,CAAC,MAAM,EAAE,CAAC;QACxB,CAAC,CAAC;IACJ,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,OAAO,wBAAwB,CAAC;AAClC,CAAC","sourcesContent":["import { useLayoutEffect, useState } from 'react';\n\nimport { NotificationResponse } from './Notifications.types';\nimport {\n addNotificationResponseReceivedListener,\n getLastNotificationResponseAsync,\n} from './NotificationsEmitter';\n\ntype MaybeNotificationResponse = NotificationResponse | null | undefined;\n\n/**\n * A React hook always returns the notification response that was received most recently\n * (a notification response designates an interaction with a notification, such as tapping on it).\n *\n * > If you don't want to use a hook, you can use `Notifications.getLastNotificationResponseAsync()` instead.\n *\n * @return The hook may return one of these three types/values:\n * - `undefined` - until we're sure of what to return,\n * - `null` - if no notification response has been received yet,\n * - a [`NotificationResponse`](#notificationresponse) object - if a notification response was received.\n *\n * @example\n * Responding to a notification tap by opening a URL that could be put into the notification's `data`\n * (opening the URL is your responsibility and is not a part of the `expo-notifications` API):\n * ```jsx\n * import * as Notifications from 'expo-notifications';\n * import { Linking } from 'react-native';\n *\n * export default function App() {\n * const lastNotificationResponse = Notifications.useLastNotificationResponse();\n * React.useEffect(() => {\n * if (\n * lastNotificationResponse &&\n * lastNotificationResponse.notification.request.content.data.url &&\n * lastNotificationResponse.actionIdentifier === Notifications.DEFAULT_ACTION_IDENTIFIER\n * ) {\n * Linking.openURL(lastNotificationResponse.notification.request.content.data.url);\n * }\n * }, [lastNotificationResponse]);\n * return (\n * // Your app content\n * );\n * }\n * ```\n * @header listen\n */\nexport default function useLastNotificationResponse() {\n const [lastNotificationResponse, setLastNotificationResponse] =\n useState<MaybeNotificationResponse>(undefined);\n\n // Pure function that returns the new response if it is different from the previous,\n // otherwise return the previous response\n const newResponseIfNeeded = (\n prevResponse: MaybeNotificationResponse,\n newResponse: MaybeNotificationResponse\n ) => {\n // If the new response is undefined or null, no need for update\n if (!newResponse) {\n return prevResponse;\n }\n // If the previous response is undefined or null and the new response is not, we should update\n if (!prevResponse) {\n return newResponse;\n }\n return prevResponse.notification.request.identifier !==\n newResponse.notification.request.identifier\n ? newResponse\n : prevResponse;\n };\n\n // useLayoutEffect ensures the listener is registered as soon as possible\n useLayoutEffect(() => {\n // Get the last response first, in case it was set earlier (even in native code on startup)\n // before this renders\n getLastNotificationResponseAsync?.().then((response) =>\n setLastNotificationResponse((prevResponse) => newResponseIfNeeded(prevResponse, response))\n );\n\n // Set up listener for responses that come in, and set the last response if needed\n const subscription = addNotificationResponseReceivedListener((response) =>\n setLastNotificationResponse((prevResponse) => newResponseIfNeeded(prevResponse, response))\n );\n return () => {\n subscription.remove();\n };\n }, []);\n\n return lastNotificationResponse;\n}\n"]}
|
|
@@ -12,6 +12,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
12
12
|
|
|
13
13
|
- (void)addDelegate:(id<EXNotificationsDelegate>)delegate;
|
|
14
14
|
- (void)removeDelegate:(id<EXNotificationsDelegate>)delegate;
|
|
15
|
+
- (nullable UNNotificationResponse *)lastNotificationResponse;
|
|
16
|
+
- (void)setLastNotificationResponse:(nullable UNNotificationResponse *)response;
|
|
15
17
|
|
|
16
18
|
@end
|
|
17
19
|
|
|
@@ -24,6 +26,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
24
26
|
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler;
|
|
25
27
|
- (void)userNotificationCenter:(UNUserNotificationCenter *)center openSettingsForNotification:(nullable UNNotification *)notification;
|
|
26
28
|
|
|
29
|
+
@property (nonatomic, strong, nullable) UNNotificationResponse *lastNotificationResponse;
|
|
30
|
+
|
|
27
31
|
@end
|
|
28
32
|
|
|
29
33
|
NS_ASSUME_NONNULL_END
|
|
@@ -123,6 +123,8 @@ EX_REGISTER_SINGLETON_MODULE(NotificationCenterDelegate);
|
|
|
123
123
|
|
|
124
124
|
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler
|
|
125
125
|
{
|
|
126
|
+
// Save last response here for use by EXNotificationsEmitter
|
|
127
|
+
self.lastNotificationResponse = response;
|
|
126
128
|
// Save response to pending responses array if none of the handlers will handle it.
|
|
127
129
|
BOOL responseWillBeHandledByAppropriateDelegate = NO;
|
|
128
130
|
for (int i = 0; i < _delegates.count; i++) {
|
|
@@ -15,8 +15,6 @@
|
|
|
15
15
|
|
|
16
16
|
@property (nonatomic, weak) id<EXEventEmitterService> eventEmitter;
|
|
17
17
|
|
|
18
|
-
@property (nonatomic, strong) UNNotificationResponse *lastNotificationResponse;
|
|
19
|
-
|
|
20
18
|
@end
|
|
21
19
|
|
|
22
20
|
@implementation EXNotificationsEmitter
|
|
@@ -26,7 +24,8 @@ EX_EXPORT_MODULE(ExpoNotificationsEmitter);
|
|
|
26
24
|
EX_EXPORT_METHOD_AS(getLastNotificationResponseAsync,
|
|
27
25
|
getLastNotificationResponseAsyncWithResolver:(EXPromiseResolveBlock)resolve reject:(EXPromiseRejectBlock)reject)
|
|
28
26
|
{
|
|
29
|
-
|
|
27
|
+
UNNotificationResponse* lastResponse = _notificationCenterDelegate.lastNotificationResponse;
|
|
28
|
+
resolve(lastResponse ? [self serializedNotificationResponse:lastResponse] : [NSNull null]);
|
|
30
29
|
}
|
|
31
30
|
|
|
32
31
|
# pragma mark - EXModuleRegistryConsumer
|
|
@@ -77,7 +76,7 @@ EX_EXPORT_METHOD_AS(getLastNotificationResponseAsync,
|
|
|
77
76
|
|
|
78
77
|
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler
|
|
79
78
|
{
|
|
80
|
-
|
|
79
|
+
_notificationCenterDelegate.lastNotificationResponse = response;
|
|
81
80
|
[self sendEventWithName:onDidReceiveNotificationResponse body:[self serializedNotificationResponse:response]];
|
|
82
81
|
completionHandler();
|
|
83
82
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-notifications",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.15",
|
|
4
4
|
"description": "Notifications module",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -55,5 +55,5 @@
|
|
|
55
55
|
"peerDependencies": {
|
|
56
56
|
"expo": "*"
|
|
57
57
|
},
|
|
58
|
-
"gitHead": "
|
|
58
|
+
"gitHead": "760c0c1a3bebcedc19836db8bc98c737f0db8c8d"
|
|
59
59
|
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useLayoutEffect, useState } from 'react';
|
|
2
2
|
|
|
3
3
|
import { NotificationResponse } from './Notifications.types';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
import {
|
|
5
|
+
addNotificationResponseReceivedListener,
|
|
6
|
+
getLastNotificationResponseAsync,
|
|
7
|
+
} from './NotificationsEmitter';
|
|
8
|
+
|
|
9
|
+
type MaybeNotificationResponse = NotificationResponse | null | undefined;
|
|
7
10
|
|
|
8
11
|
/**
|
|
9
12
|
* A React hook always returns the notification response that was received most recently
|
|
@@ -42,29 +45,45 @@ import { mapNotificationResponse } from './utils/mapNotificationResponse';
|
|
|
42
45
|
* @header listen
|
|
43
46
|
*/
|
|
44
47
|
export default function useLastNotificationResponse() {
|
|
45
|
-
const [lastNotificationResponse, setLastNotificationResponse] =
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
const [lastNotificationResponse, setLastNotificationResponse] =
|
|
49
|
+
useState<MaybeNotificationResponse>(undefined);
|
|
50
|
+
|
|
51
|
+
// Pure function that returns the new response if it is different from the previous,
|
|
52
|
+
// otherwise return the previous response
|
|
53
|
+
const newResponseIfNeeded = (
|
|
54
|
+
prevResponse: MaybeNotificationResponse,
|
|
55
|
+
newResponse: MaybeNotificationResponse
|
|
56
|
+
) => {
|
|
57
|
+
// If the new response is undefined or null, no need for update
|
|
58
|
+
if (!newResponse) {
|
|
59
|
+
return prevResponse;
|
|
60
|
+
}
|
|
61
|
+
// If the previous response is undefined or null and the new response is not, we should update
|
|
62
|
+
if (!prevResponse) {
|
|
63
|
+
return newResponse;
|
|
64
|
+
}
|
|
65
|
+
return prevResponse.notification.request.identifier !==
|
|
66
|
+
newResponse.notification.request.identifier
|
|
67
|
+
? newResponse
|
|
68
|
+
: prevResponse;
|
|
69
|
+
};
|
|
48
70
|
|
|
49
71
|
// useLayoutEffect ensures the listener is registered as soon as possible
|
|
50
72
|
useLayoutEffect(() => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
73
|
+
// Get the last response first, in case it was set earlier (even in native code on startup)
|
|
74
|
+
// before this renders
|
|
75
|
+
getLastNotificationResponseAsync?.().then((response) =>
|
|
76
|
+
setLastNotificationResponse((prevResponse) => newResponseIfNeeded(prevResponse, response))
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// Set up listener for responses that come in, and set the last response if needed
|
|
80
|
+
const subscription = addNotificationResponseReceivedListener((response) =>
|
|
81
|
+
setLastNotificationResponse((prevResponse) => newResponseIfNeeded(prevResponse, response))
|
|
82
|
+
);
|
|
55
83
|
return () => {
|
|
56
84
|
subscription.remove();
|
|
57
85
|
};
|
|
58
86
|
}, []);
|
|
59
87
|
|
|
60
|
-
// On each mount of this hook we fetch last notification response
|
|
61
|
-
// from the native module which is an "always active listener"
|
|
62
|
-
// and always returns the most recent response.
|
|
63
|
-
useEffect(() => {
|
|
64
|
-
NotificationsEmitterModule.getLastNotificationResponseAsync?.().then((response) => {
|
|
65
|
-
setLastNotificationResponse(response);
|
|
66
|
-
});
|
|
67
|
-
}, []);
|
|
68
|
-
|
|
69
88
|
return lastNotificationResponse;
|
|
70
89
|
}
|