react-native-push-signal 0.1.1 → 0.1.3
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/README.md +14 -3
- package/README.ru.md +14 -3
- package/android/build.gradle +1 -0
- package/android/consumer-rules.pro +4 -0
- package/android/src/main/java/com/margelo/nitro/pushsignal/PushSignal.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/pushsignal/PushSignalCenter.kt +170 -8
- package/android/src/main/java/com/margelo/nitro/pushsignal/PushSignalMessagingService.kt +5 -0
- package/ios/PushSignal.swift +1 -1
- package/ios/PushSignalCenter.swift +203 -10
- package/ios/PushSignalLaunchStore.m +15 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/pushSignal.js.map +1 -1
- package/lib/module/pushSignal.native.js +11 -2
- package/lib/module/pushSignal.native.js.map +1 -1
- package/lib/typescript/src/PushSignal.nitro.d.ts +2 -1
- package/lib/typescript/src/PushSignal.nitro.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/pushSignal.d.ts +2 -2
- package/lib/typescript/src/pushSignal.d.ts.map +1 -1
- package/lib/typescript/src/pushSignal.native.d.ts +2 -2
- package/lib/typescript/src/pushSignal.native.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_bool_____PushMessage.hpp +128 -0
- package/nitrogen/generated/android/c++/JHybridPushSignalSpec.cpp +5 -4
- package/nitrogen/generated/android/c++/JHybridPushSignalSpec.hpp +1 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/pushsignal/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_bool_____PushMessage.kt +78 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/pushsignal/HybridPushSignalSpec.kt +2 -2
- package/nitrogen/generated/android/pushsignalOnLoad.cpp +2 -0
- package/nitrogen/generated/ios/PushSignal-Swift-Cxx-Bridge.cpp +25 -0
- package/nitrogen/generated/ios/PushSignal-Swift-Cxx-Bridge.hpp +91 -0
- package/nitrogen/generated/ios/c++/HybridPushSignalSpecSwift.hpp +1 -1
- package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_bool_____PushMessage.swift +61 -0
- package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__shared_ptr_Promise_bool__.swift +66 -0
- package/nitrogen/generated/ios/swift/HybridPushSignalSpec.swift +1 -1
- package/nitrogen/generated/ios/swift/HybridPushSignalSpec_cxx.swift +26 -5
- package/nitrogen/generated/shared/c++/HybridPushSignalSpec.hpp +1 -1
- package/package.json +1 -1
- package/src/PushSignal.nitro.ts +5 -1
- package/src/index.tsx +1 -0
- package/src/pushSignal.native.tsx +18 -6
- package/src/pushSignal.tsx +2 -3
package/README.md
CHANGED
|
@@ -44,6 +44,10 @@ const credentials = await getCredentials();
|
|
|
44
44
|
|
|
45
45
|
const stopMessages = onMessage((message) => {
|
|
46
46
|
console.log('incoming', message);
|
|
47
|
+
if (message.data.silent === '1') {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
47
51
|
});
|
|
48
52
|
|
|
49
53
|
const stopPress = onNotificationPress((message) => {
|
|
@@ -70,7 +74,7 @@ Keep Apple `.p8` keys and the Firebase service account on the server. The app ne
|
|
|
70
74
|
2. Enable **Background Modes → Remote notifications**.
|
|
71
75
|
3. Use a physical device. The simulator cannot register with APNs.
|
|
72
76
|
|
|
73
|
-
The library hooks `UNUserNotificationCenter` and APNs token callbacks, so the host `AppDelegate` does not need extra code.
|
|
77
|
+
The library hooks `UNUserNotificationCenter` at launch (before JS starts) and APNs token callbacks, so the host `AppDelegate` does not need extra code. Foreground pushes only reach JS if this delegate is installed before launch finishes.
|
|
74
78
|
|
|
75
79
|
## Android setup
|
|
76
80
|
|
|
@@ -112,13 +116,20 @@ If the host app already declares its own `FirebaseMessagingService`, only one se
|
|
|
112
116
|
|
|
113
117
|
## Incoming messages vs taps
|
|
114
118
|
|
|
115
|
-
|
|
119
|
+
| App state | iOS | Android (notification payload) | Android (data-only) |
|
|
120
|
+
| --- | --- | --- | --- |
|
|
121
|
+
| Foreground | `onMessage`. Banner if a listener returns `true` | `onMessage`. Banner if a listener returns `true` | `onMessage`. Tray only if a listener returns `true` and the payload has title or body |
|
|
122
|
+
| Background / killed | System banner. Tap → `onNotificationPress` | System banner. Tap → `onNotificationPress` | No banner. `onMessage` if the process is alive |
|
|
123
|
+
|
|
124
|
+
- `onMessage` — the push arrived while the app is in the foreground (and Android data messages while the process is alive). Return `true` to show the system banner as usual. No return, `false`, or a throw means no banner. If several listeners are registered, the banner shows when any of them returns `true`.
|
|
116
125
|
- `onNotificationPress` — the user opened the notification, including a cold start.
|
|
117
126
|
- On iOS, a visible push received in the background or when the app is killed is delivered on tap, not through `onMessage`. That is an OS limit.
|
|
118
127
|
|
|
128
|
+
If `onMessage` throws or takes longer than about 2 seconds, the library does not show a banner.
|
|
129
|
+
|
|
119
130
|
## Web
|
|
120
131
|
|
|
121
|
-
`initialize()` resolves. `getCredentials()` throws. Listeners are no-ops.
|
|
132
|
+
`initialize()` resolves. `getCredentials()` throws. Listeners (`onMessage`, `onNotificationPress`) are no-ops.
|
|
122
133
|
|
|
123
134
|
## Contributing
|
|
124
135
|
|
package/README.ru.md
CHANGED
|
@@ -44,6 +44,10 @@ const credentials = await getCredentials();
|
|
|
44
44
|
|
|
45
45
|
const stopMessages = onMessage((message) => {
|
|
46
46
|
console.log('incoming', message);
|
|
47
|
+
if (message.data.silent === '1') {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
47
51
|
});
|
|
48
52
|
|
|
49
53
|
const stopPress = onNotificationPress((message) => {
|
|
@@ -70,7 +74,7 @@ const stopPress = onNotificationPress((message) => {
|
|
|
70
74
|
2. Включите **Background Modes → Remote notifications**.
|
|
71
75
|
3. Проверяйте на физическом устройстве. Симулятор не умеет регистрироваться в APNs.
|
|
72
76
|
|
|
73
|
-
Библиотека сама
|
|
77
|
+
Библиотека сама ставит делегат `UNUserNotificationCenter` на запуске (до JS) и подписывается на колбеки APNs-токена. В `AppDelegate` хоста ничего дописывать не нужно. Без делегата до конца запуска iOS не вызывает `willPresent`, и пуш в foreground не доходит до JS.
|
|
74
78
|
|
|
75
79
|
## Настройка Android
|
|
76
80
|
|
|
@@ -112,13 +116,20 @@ apply plugin: "com.google.gms.google-services"
|
|
|
112
116
|
|
|
113
117
|
## Входящее сообщение и тап
|
|
114
118
|
|
|
115
|
-
|
|
119
|
+
| Состояние | iOS | Android (notification payload) | Android (data-only) |
|
|
120
|
+
| --- | --- | --- | --- |
|
|
121
|
+
| Foreground | `onMessage`. Баннер, если слушатель вернул `true` | `onMessage`. Баннер, если слушатель вернул `true` | `onMessage`. Tray только если слушатель вернул `true` и в payload есть title или body |
|
|
122
|
+
| Background / killed | Системный баннер. Тап → `onNotificationPress` | Системный баннер. Тап → `onNotificationPress` | Баннера нет. `onMessage`, если процесс жив |
|
|
123
|
+
|
|
124
|
+
- `onMessage` — пуш пришёл, пока приложение на переднем плане (и Android data-message, пока процесс жив). Верните `true`, чтобы показать системный баннер как обычно. Без return, `false` или ошибка — баннера нет. Если слушателей несколько, баннер показывается, когда любой вернул `true`.
|
|
116
125
|
- `onNotificationPress` — пользователь открыл уведомление, в том числе при холодном старте.
|
|
117
126
|
- На iOS видимый пуш в фоне или при убитом приложении приходит только в тап, не в `onMessage`. Это ограничение ОС.
|
|
118
127
|
|
|
128
|
+
Если `onMessage` бросил ошибку или не ответил примерно за 2 секунды, баннер не показывается.
|
|
129
|
+
|
|
119
130
|
## Web
|
|
120
131
|
|
|
121
|
-
`initialize()` резолвится. `getCredentials()` бросает ошибку. Слушатели ничего не делают.
|
|
132
|
+
`initialize()` резолвится. `getCredentials()` бросает ошибку. Слушатели (`onMessage`, `onNotificationPress`) ничего не делают.
|
|
122
133
|
|
|
123
134
|
## Contributing
|
|
124
135
|
|
package/android/build.gradle
CHANGED
|
@@ -28,7 +28,7 @@ class PushSignal : HybridPushSignalSpec() {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
override fun setOnMessage(callback: (message: PushMessage) ->
|
|
31
|
+
override fun setOnMessage(callback: (message: PushMessage) -> Promise<Promise<Boolean>>) {
|
|
32
32
|
PushSignalCenter.setOnMessage(callback)
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -2,29 +2,44 @@ package com.margelo.nitro.pushsignal
|
|
|
2
2
|
|
|
3
3
|
import android.app.Activity
|
|
4
4
|
import android.app.Application
|
|
5
|
+
import android.app.NotificationChannel
|
|
6
|
+
import android.app.NotificationManager
|
|
7
|
+
import android.app.PendingIntent
|
|
5
8
|
import android.content.Context
|
|
6
9
|
import android.content.Intent
|
|
10
|
+
import android.os.Build
|
|
7
11
|
import android.os.Bundle
|
|
12
|
+
import android.os.Handler
|
|
13
|
+
import android.os.Looper
|
|
8
14
|
import androidx.activity.ComponentActivity
|
|
15
|
+
import androidx.core.app.NotificationCompat
|
|
9
16
|
import com.google.firebase.FirebaseApp
|
|
10
17
|
import com.google.firebase.FirebaseOptions
|
|
11
18
|
import com.google.firebase.messaging.FirebaseMessaging
|
|
12
19
|
import com.google.android.gms.tasks.Tasks
|
|
13
20
|
import com.google.firebase.messaging.RemoteMessage
|
|
21
|
+
import com.margelo.nitro.core.Promise
|
|
14
22
|
import java.util.Collections
|
|
23
|
+
import java.util.UUID
|
|
15
24
|
import java.util.WeakHashMap
|
|
16
25
|
import java.util.concurrent.CopyOnWriteArrayList
|
|
26
|
+
import java.util.concurrent.atomic.AtomicBoolean
|
|
17
27
|
|
|
18
28
|
internal object PushSignalCenter : Application.ActivityLifecycleCallbacks {
|
|
19
29
|
private const val EXTRA_HANDLED = "pushsignal.handled"
|
|
30
|
+
private const val CHANNEL_ID = "push_signal_default"
|
|
31
|
+
private const val FOREGROUND_TIMEOUT_MS = 2_000L
|
|
20
32
|
|
|
21
33
|
private val lock = Any()
|
|
34
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
22
35
|
@Volatile private var application: Application? = null
|
|
23
36
|
@Volatile private var currentActivity: Activity? = null
|
|
24
|
-
@Volatile private var onMessage: ((PushMessage) ->
|
|
37
|
+
@Volatile private var onMessage: ((PushMessage) -> Promise<Promise<Boolean>>)? = null
|
|
25
38
|
@Volatile private var onNotificationPress: ((PushMessage) -> Unit)? = null
|
|
26
39
|
@Volatile private var pendingPress: PushMessage? = null
|
|
40
|
+
private val pendingMessages = CopyOnWriteArrayList<PushMessage>()
|
|
27
41
|
private val registeredActivities = Collections.newSetFromMap(WeakHashMap<Activity, Boolean>())
|
|
42
|
+
@Volatile private var startedActivityCount = 0
|
|
28
43
|
@Volatile private var pendingFirebaseConfig: AndroidFirebaseConfig? = null
|
|
29
44
|
private val initializeWaiters = CopyOnWriteArrayList<(Exception?) -> Unit>()
|
|
30
45
|
|
|
@@ -63,8 +78,24 @@ internal object PushSignalCenter : Application.ActivityLifecycleCallbacks {
|
|
|
63
78
|
onDone(applyFirebaseConfig(context, config))
|
|
64
79
|
}
|
|
65
80
|
|
|
66
|
-
fun setOnMessage(callback: (PushMessage) ->
|
|
81
|
+
fun setOnMessage(callback: (PushMessage) -> Promise<Promise<Boolean>>) {
|
|
67
82
|
onMessage = callback
|
|
83
|
+
val queued = pendingMessages.toList()
|
|
84
|
+
pendingMessages.clear()
|
|
85
|
+
queued.forEach { message ->
|
|
86
|
+
runOnMain {
|
|
87
|
+
val inForeground = startedActivityCount > 0 || currentActivity != null
|
|
88
|
+
resolveShouldShowBanner(message) { shouldShow ->
|
|
89
|
+
if (
|
|
90
|
+
shouldShow &&
|
|
91
|
+
inForeground &&
|
|
92
|
+
(!message.title.isNullOrEmpty() || !message.body.isNullOrEmpty())
|
|
93
|
+
) {
|
|
94
|
+
postForegroundNotification(message)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
68
99
|
}
|
|
69
100
|
|
|
70
101
|
fun setOnNotificationPress(callback: (PushMessage) -> Unit) {
|
|
@@ -112,12 +143,133 @@ internal object PushSignalCenter : Application.ActivityLifecycleCallbacks {
|
|
|
112
143
|
return token
|
|
113
144
|
}
|
|
114
145
|
|
|
115
|
-
fun emitMessage(
|
|
116
|
-
|
|
146
|
+
fun emitMessage(remoteMessage: RemoteMessage) {
|
|
147
|
+
val message = remoteMessage.toPushMessage()
|
|
148
|
+
runOnMain {
|
|
149
|
+
val callback = onMessage
|
|
150
|
+
if (callback == null) {
|
|
151
|
+
pendingMessages.add(message)
|
|
152
|
+
return@runOnMain
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
val inForeground = startedActivityCount > 0 || currentActivity != null
|
|
156
|
+
resolveShouldShowBanner(message) { shouldShow ->
|
|
157
|
+
if (
|
|
158
|
+
shouldShow &&
|
|
159
|
+
inForeground &&
|
|
160
|
+
(!message.title.isNullOrEmpty() || !message.body.isNullOrEmpty())
|
|
161
|
+
) {
|
|
162
|
+
postForegroundNotification(message)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
117
166
|
}
|
|
118
167
|
|
|
119
|
-
fun
|
|
120
|
-
|
|
168
|
+
private fun runOnMain(block: () -> Unit) {
|
|
169
|
+
if (Looper.myLooper() == Looper.getMainLooper()) {
|
|
170
|
+
block()
|
|
171
|
+
} else {
|
|
172
|
+
mainHandler.post(block)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private fun resolveShouldShowBanner(
|
|
177
|
+
message: PushMessage,
|
|
178
|
+
onDone: (Boolean) -> Unit
|
|
179
|
+
) {
|
|
180
|
+
val callback = onMessage
|
|
181
|
+
if (callback == null) {
|
|
182
|
+
onDone(false)
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
val delivered = AtomicBoolean(false)
|
|
187
|
+
val timeout = Runnable {
|
|
188
|
+
if (delivered.compareAndSet(false, true)) {
|
|
189
|
+
onDone(true)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
mainHandler.postDelayed(timeout, FOREGROUND_TIMEOUT_MS)
|
|
193
|
+
|
|
194
|
+
val finish = { shouldShow: Boolean ->
|
|
195
|
+
if (delivered.compareAndSet(false, true)) {
|
|
196
|
+
mainHandler.removeCallbacks(timeout)
|
|
197
|
+
onDone(shouldShow)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
callback(message)
|
|
203
|
+
.then { inner ->
|
|
204
|
+
inner
|
|
205
|
+
.then { shouldShow -> finish(shouldShow) }
|
|
206
|
+
.catch { finish(true) }
|
|
207
|
+
}
|
|
208
|
+
.catch { finish(true) }
|
|
209
|
+
} catch (_: Throwable) {
|
|
210
|
+
finish(true)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private fun postForegroundNotification(message: PushMessage) {
|
|
215
|
+
val context = application ?: return
|
|
216
|
+
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
|
|
217
|
+
?: return
|
|
218
|
+
|
|
219
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
220
|
+
val existing = manager.getNotificationChannel(CHANNEL_ID)
|
|
221
|
+
if (existing == null) {
|
|
222
|
+
manager.createNotificationChannel(
|
|
223
|
+
NotificationChannel(
|
|
224
|
+
CHANNEL_ID,
|
|
225
|
+
"Notifications",
|
|
226
|
+
NotificationManager.IMPORTANCE_HIGH
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
|
|
233
|
+
?: return
|
|
234
|
+
launchIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
|
235
|
+
launchIntent.putExtra("google.message_id", message.id ?: UUID.randomUUID().toString())
|
|
236
|
+
message.title?.let { launchIntent.putExtra("gcm.notification.title", it) }
|
|
237
|
+
message.body?.let { launchIntent.putExtra("gcm.notification.body", it) }
|
|
238
|
+
message.data.forEach { (key, value) ->
|
|
239
|
+
launchIntent.putExtra(key, value)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
val requestCode = (message.id ?: message.title ?: "push").hashCode()
|
|
243
|
+
val pendingIntent = PendingIntent.getActivity(
|
|
244
|
+
context,
|
|
245
|
+
requestCode,
|
|
246
|
+
launchIntent,
|
|
247
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
|
|
251
|
+
.setSmallIcon(smallIcon(context))
|
|
252
|
+
.setContentTitle(message.title.orEmpty())
|
|
253
|
+
.setContentText(message.body.orEmpty())
|
|
254
|
+
.setContentIntent(pendingIntent)
|
|
255
|
+
.setAutoCancel(true)
|
|
256
|
+
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
|
257
|
+
.setDefaults(NotificationCompat.DEFAULT_SOUND)
|
|
258
|
+
.setNumber(1)
|
|
259
|
+
|
|
260
|
+
manager.notify(requestCode, builder.build())
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private fun smallIcon(context: Context): Int {
|
|
264
|
+
val named = context.resources.getIdentifier("ic_notification", "drawable", context.packageName)
|
|
265
|
+
if (named != 0) {
|
|
266
|
+
return named
|
|
267
|
+
}
|
|
268
|
+
val appIcon = context.applicationInfo.icon
|
|
269
|
+
if (appIcon != 0) {
|
|
270
|
+
return appIcon
|
|
271
|
+
}
|
|
272
|
+
return android.R.drawable.stat_notify_more
|
|
121
273
|
}
|
|
122
274
|
|
|
123
275
|
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
|
@@ -126,7 +278,10 @@ internal object PushSignalCenter : Application.ActivityLifecycleCallbacks {
|
|
|
126
278
|
handleIntent(activity.intent)
|
|
127
279
|
}
|
|
128
280
|
|
|
129
|
-
override fun onActivityStarted(activity: Activity)
|
|
281
|
+
override fun onActivityStarted(activity: Activity) {
|
|
282
|
+
currentActivity = activity
|
|
283
|
+
startedActivityCount += 1
|
|
284
|
+
}
|
|
130
285
|
|
|
131
286
|
override fun onActivityResumed(activity: Activity) {
|
|
132
287
|
currentActivity = activity
|
|
@@ -139,7 +294,12 @@ internal object PushSignalCenter : Application.ActivityLifecycleCallbacks {
|
|
|
139
294
|
}
|
|
140
295
|
}
|
|
141
296
|
|
|
142
|
-
override fun onActivityStopped(activity: Activity)
|
|
297
|
+
override fun onActivityStopped(activity: Activity) {
|
|
298
|
+
startedActivityCount = (startedActivityCount - 1).coerceAtLeast(0)
|
|
299
|
+
if (currentActivity === activity) {
|
|
300
|
+
currentActivity = null
|
|
301
|
+
}
|
|
302
|
+
}
|
|
143
303
|
|
|
144
304
|
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit
|
|
145
305
|
|
|
@@ -171,6 +331,7 @@ internal object PushSignalCenter : Application.ActivityLifecycleCallbacks {
|
|
|
171
331
|
|
|
172
332
|
private fun applyFirebaseConfig(context: Context, config: AndroidFirebaseConfig): Exception? {
|
|
173
333
|
if (FirebaseApp.getApps(context).isNotEmpty()) {
|
|
334
|
+
FirebaseMessaging.getInstance().isAutoInitEnabled = true
|
|
174
335
|
return null
|
|
175
336
|
}
|
|
176
337
|
|
|
@@ -187,6 +348,7 @@ internal object PushSignalCenter : Application.ActivityLifecycleCallbacks {
|
|
|
187
348
|
.setGcmSenderId(config.project_number!!.trim())
|
|
188
349
|
.build()
|
|
189
350
|
FirebaseApp.initializeApp(context, options)
|
|
351
|
+
FirebaseMessaging.getInstance().isAutoInitEnabled = true
|
|
190
352
|
null
|
|
191
353
|
} catch (error: Exception) {
|
|
192
354
|
error
|
|
@@ -9,7 +9,12 @@ class PushSignalMessagingService : FirebaseMessagingService() {
|
|
|
9
9
|
PushSignalCenter.attach(applicationContext)
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
override fun onNewToken(token: String) {
|
|
13
|
+
PushSignalCenter.attach(applicationContext)
|
|
14
|
+
}
|
|
15
|
+
|
|
12
16
|
override fun onMessageReceived(message: RemoteMessage) {
|
|
17
|
+
PushSignalCenter.attach(applicationContext)
|
|
13
18
|
PushSignalCenter.emitMessage(message)
|
|
14
19
|
}
|
|
15
20
|
}
|
package/ios/PushSignal.swift
CHANGED
|
@@ -19,7 +19,7 @@ class PushSignal: HybridPushSignalSpec {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
func setOnMessage(callback: @escaping (PushMessage) ->
|
|
22
|
+
func setOnMessage(callback: @escaping (PushMessage) -> Promise<Promise<Bool>>) throws {
|
|
23
23
|
PushSignalCenter.shared.onMessage = callback
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Foundation
|
|
2
|
+
import NitroModules
|
|
2
3
|
import ObjectiveC
|
|
3
4
|
import UIKit
|
|
4
5
|
import UserNotifications
|
|
@@ -12,16 +13,31 @@ final class PushSignalCenter: NSObject, UNUserNotificationCenterDelegate {
|
|
|
12
13
|
shared.install()
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
/// Called from ObjC `+load` / launch notifications so the UNUserNotificationCenter
|
|
17
|
+
/// delegate is set before the app finishes launching. If this happens after JS loads,
|
|
18
|
+
/// iOS never calls `willPresent` and foreground pushes never reach JS.
|
|
19
|
+
@objc static func installEarly() {
|
|
20
|
+
shared.install()
|
|
21
|
+
}
|
|
22
|
+
|
|
15
23
|
private let lock = NSLock()
|
|
16
24
|
private var didInstall = false
|
|
25
|
+
private var didSwizzleNotificationCenter = false
|
|
17
26
|
private var deviceToken: String?
|
|
18
27
|
private var registrationError: Error?
|
|
19
28
|
private var tokenWaiters: [(Result<String, Error>) -> Void] = []
|
|
20
29
|
private var pendingPress: PushMessage?
|
|
30
|
+
private var pendingMessages: [PushMessage] = []
|
|
21
31
|
private var pendingLaunchOptions: [AnyHashable: Any]?
|
|
22
32
|
private var didCaptureLaunchNotification = false
|
|
33
|
+
private var recentMessageIds: [String: Date] = [:]
|
|
34
|
+
private weak var forwardingDelegate: UNUserNotificationCenterDelegate?
|
|
23
35
|
|
|
24
|
-
var onMessage: ((PushMessage) ->
|
|
36
|
+
var onMessage: ((PushMessage) -> Promise<Promise<Bool>>)? {
|
|
37
|
+
didSet {
|
|
38
|
+
flushPendingMessages()
|
|
39
|
+
}
|
|
40
|
+
}
|
|
25
41
|
var onNotificationPress: ((PushMessage) -> Void)? {
|
|
26
42
|
didSet {
|
|
27
43
|
flushPendingPress()
|
|
@@ -29,13 +45,21 @@ final class PushSignalCenter: NSObject, UNUserNotificationCenterDelegate {
|
|
|
29
45
|
}
|
|
30
46
|
|
|
31
47
|
func install() {
|
|
32
|
-
|
|
33
|
-
|
|
48
|
+
if Thread.isMainThread {
|
|
49
|
+
installOnMain()
|
|
50
|
+
} else {
|
|
51
|
+
DispatchQueue.main.async {
|
|
52
|
+
self.installOnMain()
|
|
53
|
+
}
|
|
34
54
|
}
|
|
35
55
|
}
|
|
36
56
|
|
|
37
57
|
private func installOnMain() {
|
|
58
|
+
swizzleNotificationCenterDelegateIfNeeded()
|
|
59
|
+
|
|
38
60
|
guard !didInstall else {
|
|
61
|
+
UNUserNotificationCenter.current().delegate = self
|
|
62
|
+
swizzleAppDelegate()
|
|
39
63
|
captureLaunchNotificationIfNeeded()
|
|
40
64
|
return
|
|
41
65
|
}
|
|
@@ -46,6 +70,12 @@ final class PushSignalCenter: NSObject, UNUserNotificationCenterDelegate {
|
|
|
46
70
|
captureLaunchNotificationIfNeeded()
|
|
47
71
|
}
|
|
48
72
|
|
|
73
|
+
func rememberForwardingDelegate(_ delegate: UNUserNotificationCenterDelegate?) {
|
|
74
|
+
if let delegate, delegate !== self {
|
|
75
|
+
forwardingDelegate = delegate
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
49
79
|
func fetchCredentials() async throws -> PushCredentials {
|
|
50
80
|
await MainActor.run {
|
|
51
81
|
self.installOnMain()
|
|
@@ -69,22 +99,39 @@ final class PushSignalCenter: NSObject, UNUserNotificationCenterDelegate {
|
|
|
69
99
|
finishRegistration(result: .failure(error))
|
|
70
100
|
}
|
|
71
101
|
|
|
72
|
-
func userNotificationCenter(
|
|
102
|
+
@objc func userNotificationCenter(
|
|
73
103
|
_ center: UNUserNotificationCenter,
|
|
74
104
|
willPresent notification: UNNotification,
|
|
75
105
|
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
|
|
76
106
|
) {
|
|
77
|
-
|
|
78
|
-
|
|
107
|
+
let message = Self.message(from: notification)
|
|
108
|
+
Task {
|
|
109
|
+
let shouldShow = await self.deliverMessage(message)
|
|
110
|
+
let ours: UNNotificationPresentationOptions = shouldShow ? Self.foregroundPresentationOptions : []
|
|
111
|
+
await MainActor.run {
|
|
112
|
+
self.forwardWillPresent(
|
|
113
|
+
center,
|
|
114
|
+
notification: notification,
|
|
115
|
+
ourOptions: ours,
|
|
116
|
+
completionHandler: completionHandler
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
79
120
|
}
|
|
80
121
|
|
|
81
|
-
func userNotificationCenter(
|
|
122
|
+
@objc func userNotificationCenter(
|
|
82
123
|
_ center: UNUserNotificationCenter,
|
|
83
124
|
didReceive response: UNNotificationResponse,
|
|
84
125
|
withCompletionHandler completionHandler: @escaping () -> Void
|
|
85
126
|
) {
|
|
86
127
|
emitPress(Self.message(from: response.notification))
|
|
87
|
-
|
|
128
|
+
if forwardingDelegate?.responds(
|
|
129
|
+
to: #selector(UNUserNotificationCenterDelegate.userNotificationCenter(_:didReceive:withCompletionHandler:))
|
|
130
|
+
) == true {
|
|
131
|
+
forwardingDelegate?.userNotificationCenter?(center, didReceive: response, withCompletionHandler: completionHandler)
|
|
132
|
+
} else {
|
|
133
|
+
completionHandler()
|
|
134
|
+
}
|
|
88
135
|
}
|
|
89
136
|
|
|
90
137
|
private func waitForToken() async throws -> String {
|
|
@@ -152,10 +199,125 @@ final class PushSignalCenter: NSObject, UNUserNotificationCenterDelegate {
|
|
|
152
199
|
waiters.forEach { $0(result) }
|
|
153
200
|
}
|
|
154
201
|
|
|
155
|
-
private func
|
|
202
|
+
private func synchronized<T>(_ body: () -> T) -> T {
|
|
203
|
+
lock.lock()
|
|
204
|
+
defer { lock.unlock() }
|
|
205
|
+
return body()
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private func deliverMessage(_ message: PushMessage) async -> Bool {
|
|
209
|
+
guard shouldEmit(message) else {
|
|
210
|
+
return false
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let callback = synchronized { onMessage }
|
|
214
|
+
|
|
215
|
+
guard let callback else {
|
|
216
|
+
synchronized { pendingMessages.append(message) }
|
|
217
|
+
// Still show the system banner so a visible push is not swallowed
|
|
218
|
+
// before JS has subscribed.
|
|
219
|
+
return true
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return await invokeOnMessage(callback, message: message)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private func invokeOnMessage(
|
|
226
|
+
_ callback: @escaping (PushMessage) -> Promise<Promise<Bool>>,
|
|
227
|
+
message: PushMessage
|
|
228
|
+
) async -> Bool {
|
|
229
|
+
await withCheckedContinuation { continuation in
|
|
230
|
+
let resumeLock = NSLock()
|
|
231
|
+
var resumed = false
|
|
232
|
+
let finish: (Bool) -> Void = { value in
|
|
233
|
+
resumeLock.lock()
|
|
234
|
+
defer { resumeLock.unlock() }
|
|
235
|
+
guard !resumed else {
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
resumed = true
|
|
239
|
+
continuation.resume(returning: value)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
DispatchQueue.main.async {
|
|
243
|
+
Task {
|
|
244
|
+
do {
|
|
245
|
+
let inner = try await callback(message).await()
|
|
246
|
+
let shouldShow = try await inner.await()
|
|
247
|
+
finish(shouldShow)
|
|
248
|
+
} catch {
|
|
249
|
+
finish(true)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
|
255
|
+
finish(true)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private func flushPendingMessages() {
|
|
156
261
|
DispatchQueue.main.async {
|
|
157
|
-
self.onMessage
|
|
262
|
+
let callback = self.synchronized { self.onMessage }
|
|
263
|
+
let queued = self.synchronized { () -> [PushMessage] in
|
|
264
|
+
let messages = self.pendingMessages
|
|
265
|
+
self.pendingMessages.removeAll()
|
|
266
|
+
return messages
|
|
267
|
+
}
|
|
268
|
+
guard let callback, !queued.isEmpty else {
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
for message in queued {
|
|
273
|
+
Task {
|
|
274
|
+
_ = await self.invokeOnMessage(callback, message: message)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private func shouldEmit(_ message: PushMessage) -> Bool {
|
|
281
|
+
let key = message.id ?? "\(message.title ?? "")|\(message.body ?? "")"
|
|
282
|
+
return synchronized {
|
|
283
|
+
let now = Date()
|
|
284
|
+
recentMessageIds = recentMessageIds.filter { now.timeIntervalSince($0.value) < 5 }
|
|
285
|
+
if let last = recentMessageIds[key], now.timeIntervalSince(last) < 2 {
|
|
286
|
+
return false
|
|
287
|
+
}
|
|
288
|
+
recentMessageIds[key] = now
|
|
289
|
+
return true
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private func forwardWillPresent(
|
|
294
|
+
_ center: UNUserNotificationCenter,
|
|
295
|
+
notification: UNNotification,
|
|
296
|
+
ourOptions: UNNotificationPresentationOptions,
|
|
297
|
+
completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
|
|
298
|
+
) {
|
|
299
|
+
let selector = #selector(
|
|
300
|
+
UNUserNotificationCenterDelegate.userNotificationCenter(_:willPresent:withCompletionHandler:)
|
|
301
|
+
)
|
|
302
|
+
guard let forwardingDelegate, forwardingDelegate.responds(to: selector) else {
|
|
303
|
+
completionHandler(ourOptions)
|
|
304
|
+
return
|
|
158
305
|
}
|
|
306
|
+
|
|
307
|
+
forwardingDelegate.userNotificationCenter?(
|
|
308
|
+
center,
|
|
309
|
+
willPresent: notification,
|
|
310
|
+
withCompletionHandler: { forwarded in
|
|
311
|
+
completionHandler(ourOptions.union(forwarded))
|
|
312
|
+
}
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private static var foregroundPresentationOptions: UNNotificationPresentationOptions {
|
|
317
|
+
if #available(iOS 14.0, *) {
|
|
318
|
+
return [.banner, .list, .sound, .badge]
|
|
319
|
+
}
|
|
320
|
+
return [.alert, .sound, .badge]
|
|
159
321
|
}
|
|
160
322
|
|
|
161
323
|
private func emitPress(_ message: PushMessage) {
|
|
@@ -333,6 +495,25 @@ final class PushSignalCenter: NSObject, UNUserNotificationCenterDelegate {
|
|
|
333
495
|
)
|
|
334
496
|
}
|
|
335
497
|
|
|
498
|
+
private func swizzleNotificationCenterDelegateIfNeeded() {
|
|
499
|
+
guard !didSwizzleNotificationCenter else {
|
|
500
|
+
return
|
|
501
|
+
}
|
|
502
|
+
didSwizzleNotificationCenter = true
|
|
503
|
+
|
|
504
|
+
let target: AnyClass = UNUserNotificationCenter.self
|
|
505
|
+
let original = #selector(setter: UNUserNotificationCenter.delegate)
|
|
506
|
+
let replacement = #selector(UNUserNotificationCenter.pushSignal_setDelegate(_:))
|
|
507
|
+
|
|
508
|
+
guard let originalMethod = class_getInstanceMethod(target, original),
|
|
509
|
+
let replacementMethod = class_getInstanceMethod(target, replacement)
|
|
510
|
+
else {
|
|
511
|
+
return
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
method_exchangeImplementations(originalMethod, replacementMethod)
|
|
515
|
+
}
|
|
516
|
+
|
|
336
517
|
private func swizzle(target: AnyClass, original: Selector, replacement: Selector, source: AnyClass) {
|
|
337
518
|
guard let replacementMethod = class_getInstanceMethod(source, replacement) else {
|
|
338
519
|
return
|
|
@@ -393,3 +574,15 @@ private final class PushSignalAppDelegateHook: NSObject {
|
|
|
393
574
|
unsafeBitCast(method_getImplementation(method), to: Fn.self)(self, selector, application, error as NSError)
|
|
394
575
|
}
|
|
395
576
|
}
|
|
577
|
+
|
|
578
|
+
extension UNUserNotificationCenter {
|
|
579
|
+
@objc func pushSignal_setDelegate(_ delegate: UNUserNotificationCenterDelegate?) {
|
|
580
|
+
if let delegate, delegate is PushSignalCenter {
|
|
581
|
+
pushSignal_setDelegate(delegate)
|
|
582
|
+
return
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
PushSignalCenter.shared.rememberForwardingDelegate(delegate)
|
|
586
|
+
pushSignal_setDelegate(PushSignalCenter.shared)
|
|
587
|
+
}
|
|
588
|
+
}
|