react-native-push-signal 0.1.0 → 0.1.2
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 +36 -19
- package/README.ru.md +36 -19
- package/android/src/main/java/com/margelo/nitro/pushsignal/PushSignal.kt +10 -16
- package/android/src/main/java/com/margelo/nitro/pushsignal/PushSignalCenter.kt +156 -109
- package/ios/PushSignal.swift +3 -12
- package/ios/PushSignalCenter.swift +36 -41
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/pushSignal.js +1 -7
- package/lib/module/pushSignal.js.map +1 -1
- package/lib/module/pushSignal.native.js +10 -9
- package/lib/module/pushSignal.native.js.map +1 -1
- package/lib/typescript/src/PushSignal.nitro.d.ts +9 -10
- package/lib/typescript/src/PushSignal.nitro.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/pushSignal.d.ts +3 -5
- package/lib/typescript/src/pushSignal.d.ts.map +1 -1
- package/lib/typescript/src/pushSignal.native.d.ts +3 -5
- package/lib/typescript/src/pushSignal.native.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JAndroidFirebaseConfig.hpp +16 -16
- 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 +12 -35
- package/nitrogen/generated/android/c++/JHybridPushSignalSpec.hpp +2 -4
- package/nitrogen/generated/android/c++/JPushPlatform.hpp +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/pushsignal/AndroidFirebaseConfig.kt +14 -14
- 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 +3 -11
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/pushsignal/PushPlatform.kt +1 -1
- package/nitrogen/generated/android/pushsignalOnLoad.cpp +2 -0
- package/nitrogen/generated/ios/PushSignal-Swift-Cxx-Bridge.cpp +30 -5
- package/nitrogen/generated/ios/PushSignal-Swift-Cxx-Bridge.hpp +140 -52
- package/nitrogen/generated/ios/PushSignal-Swift-Cxx-Umbrella.hpp +0 -3
- package/nitrogen/generated/ios/c++/HybridPushSignalSpecSwift.hpp +3 -20
- package/nitrogen/generated/ios/swift/AndroidFirebaseConfig.swift +17 -17
- 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_PermissionStatus.swift → Func_void.swift} +11 -11
- 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 +2 -4
- package/nitrogen/generated/ios/swift/HybridPushSignalSpec_cxx.swift +34 -43
- package/nitrogen/generated/ios/swift/PushPlatform.swift +4 -4
- package/nitrogen/generated/shared/c++/AndroidFirebaseConfig.hpp +17 -17
- package/nitrogen/generated/shared/c++/HybridPushSignalSpec.cpp +0 -2
- package/nitrogen/generated/shared/c++/HybridPushSignalSpec.hpp +3 -8
- package/nitrogen/generated/shared/c++/PushPlatform.hpp +4 -4
- package/package.json +2 -2
- package/src/PushSignal.nitro.ts +12 -10
- package/src/index.tsx +1 -3
- package/src/pushSignal.native.tsx +17 -17
- package/src/pushSignal.tsx +5 -13
- package/nitrogen/generated/android/c++/JPermissionStatus.hpp +0 -61
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/pushsignal/PermissionStatus.kt +0 -24
- package/nitrogen/generated/ios/swift/PermissionStatus.swift +0 -44
- package/nitrogen/generated/shared/c++/PermissionStatus.hpp +0 -80
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
English | [Русский](README.ru.md)
|
|
4
4
|
|
|
5
|
-
Get
|
|
5
|
+
Get device credentials for your server and listen for incoming notifications or taps. The library does not send pushes — your backend talks to APNs and FCM. Request notification permission yourself with [`react-native-permissions`](https://github.com/zoontek/react-native-permissions).
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -15,29 +15,39 @@ npm install react-native-push-signal react-native-nitro-modules
|
|
|
15
15
|
## Usage
|
|
16
16
|
|
|
17
17
|
```ts
|
|
18
|
+
import {
|
|
19
|
+
checkNotifications,
|
|
20
|
+
requestNotifications,
|
|
21
|
+
RESULTS,
|
|
22
|
+
} from 'react-native-permissions';
|
|
18
23
|
import {
|
|
19
24
|
getCredentials,
|
|
20
|
-
getPermissionStatus,
|
|
21
25
|
initialize,
|
|
22
26
|
onMessage,
|
|
23
27
|
onNotificationPress,
|
|
24
|
-
requestPermission,
|
|
25
28
|
} from 'react-native-push-signal';
|
|
26
29
|
|
|
27
|
-
initialize({
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
await initialize({
|
|
31
|
+
project_id: 'my-project',
|
|
32
|
+
mobilesdk_app_id: '1:123456789:android:abcd',
|
|
33
|
+
current_key: 'AIza...',
|
|
34
|
+
project_number: '123456789',
|
|
32
35
|
});
|
|
33
36
|
|
|
34
|
-
const status = await
|
|
35
|
-
|
|
37
|
+
const { status } = await checkNotifications();
|
|
38
|
+
if (status !== RESULTS.GRANTED) {
|
|
39
|
+
await requestNotifications(['alert', 'badge', 'sound']);
|
|
40
|
+
}
|
|
41
|
+
|
|
36
42
|
const credentials = await getCredentials();
|
|
37
43
|
// POST credentials to your server: { platform, token, environment? }
|
|
38
44
|
|
|
39
45
|
const stopMessages = onMessage((message) => {
|
|
40
46
|
console.log('incoming', message);
|
|
47
|
+
if (message.data.silent === '1') {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
41
51
|
});
|
|
42
52
|
|
|
43
53
|
const stopPress = onNotificationPress((message) => {
|
|
@@ -75,15 +85,15 @@ Remote Android pushes go through FCM. You can either pass the client Firebase fi
|
|
|
75
85
|
Call `initialize` with values from `google-services.json` (not a service account):
|
|
76
86
|
|
|
77
87
|
```ts
|
|
78
|
-
initialize({
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
88
|
+
await initialize({
|
|
89
|
+
project_id: '...',
|
|
90
|
+
mobilesdk_app_id: '...',
|
|
91
|
+
current_key: '...',
|
|
92
|
+
project_number: '...',
|
|
83
93
|
});
|
|
84
94
|
```
|
|
85
95
|
|
|
86
|
-
All four fields are required for Firebase to start. If any is missing, `initialize`
|
|
96
|
+
All four fields are required for Firebase to start. If any is missing, `initialize` resolves and does nothing. iOS ignores the config and still resolves the promise.
|
|
87
97
|
|
|
88
98
|
Keep the Firebase service account on your server. Do not put it in the app.
|
|
89
99
|
|
|
@@ -100,19 +110,26 @@ apply plugin: "com.google.gms.google-services"
|
|
|
100
110
|
|
|
101
111
|
3. Add the plugin classpath in `android/build.gradle` / `settings.gradle` as in the [Firebase Android setup](https://firebase.google.com/docs/android/setup).
|
|
102
112
|
|
|
103
|
-
Without `initialize(...)` or that file, `getCredentials()` throws.
|
|
113
|
+
Without `initialize(...)` or that file, `getCredentials()` throws.
|
|
104
114
|
|
|
105
115
|
If the host app already declares its own `FirebaseMessagingService`, only one service can handle `com.google.firebase.MESSAGING_EVENT`. Prefer this library’s service or forward events into it.
|
|
106
116
|
|
|
107
117
|
## Incoming messages vs taps
|
|
108
118
|
|
|
109
|
-
|
|
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`.
|
|
110
125
|
- `onNotificationPress` — the user opened the notification, including a cold start.
|
|
111
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.
|
|
112
127
|
|
|
128
|
+
If `onMessage` throws or takes longer than about 2 seconds, the library does not show a banner.
|
|
129
|
+
|
|
113
130
|
## Web
|
|
114
131
|
|
|
115
|
-
|
|
132
|
+
`initialize()` resolves. `getCredentials()` throws. Listeners (`onMessage`, `onNotificationPress`) are no-ops.
|
|
116
133
|
|
|
117
134
|
## Contributing
|
|
118
135
|
|
package/README.ru.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | Русский
|
|
4
4
|
|
|
5
|
-
Библиотека
|
|
5
|
+
Библиотека отдаёт токен устройства для вашего сервера и сообщает о входящем уведомлении или нажатии на него. Сами пуши она не отправляет — в APNs и FCM ходит бэкенд. Разрешение на уведомления запрашивайте сами через [`react-native-permissions`](https://github.com/zoontek/react-native-permissions).
|
|
6
6
|
|
|
7
7
|
## Установка
|
|
8
8
|
|
|
@@ -15,29 +15,39 @@ npm install react-native-push-signal react-native-nitro-modules
|
|
|
15
15
|
## Использование
|
|
16
16
|
|
|
17
17
|
```ts
|
|
18
|
+
import {
|
|
19
|
+
checkNotifications,
|
|
20
|
+
requestNotifications,
|
|
21
|
+
RESULTS,
|
|
22
|
+
} from 'react-native-permissions';
|
|
18
23
|
import {
|
|
19
24
|
getCredentials,
|
|
20
|
-
getPermissionStatus,
|
|
21
25
|
initialize,
|
|
22
26
|
onMessage,
|
|
23
27
|
onNotificationPress,
|
|
24
|
-
requestPermission,
|
|
25
28
|
} from 'react-native-push-signal';
|
|
26
29
|
|
|
27
|
-
initialize({
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
await initialize({
|
|
31
|
+
project_id: 'my-project',
|
|
32
|
+
mobilesdk_app_id: '1:123456789:android:abcd',
|
|
33
|
+
current_key: 'AIza...',
|
|
34
|
+
project_number: '123456789',
|
|
32
35
|
});
|
|
33
36
|
|
|
34
|
-
const status = await
|
|
35
|
-
|
|
37
|
+
const { status } = await checkNotifications();
|
|
38
|
+
if (status !== RESULTS.GRANTED) {
|
|
39
|
+
await requestNotifications(['alert', 'badge', 'sound']);
|
|
40
|
+
}
|
|
41
|
+
|
|
36
42
|
const credentials = await getCredentials();
|
|
37
43
|
// POST credentials на сервер: { platform, token, environment? }
|
|
38
44
|
|
|
39
45
|
const stopMessages = onMessage((message) => {
|
|
40
46
|
console.log('incoming', message);
|
|
47
|
+
if (message.data.silent === '1') {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
41
51
|
});
|
|
42
52
|
|
|
43
53
|
const stopPress = onNotificationPress((message) => {
|
|
@@ -75,15 +85,15 @@ const stopPress = onNotificationPress((message) => {
|
|
|
75
85
|
Вызовите `initialize` со значениями из `google-services.json` (не из service account):
|
|
76
86
|
|
|
77
87
|
```ts
|
|
78
|
-
initialize({
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
88
|
+
await initialize({
|
|
89
|
+
project_id: '...',
|
|
90
|
+
mobilesdk_app_id: '...',
|
|
91
|
+
current_key: '...',
|
|
92
|
+
project_number: '...',
|
|
83
93
|
});
|
|
84
94
|
```
|
|
85
95
|
|
|
86
|
-
Нужны все четыре поля. Если чего-то нет, `initialize`
|
|
96
|
+
Нужны все четыре поля. Если чего-то нет, `initialize` резолвится и ничего не делает. На iOS конфиг игнорируется, промис всё равно резолвится.
|
|
87
97
|
|
|
88
98
|
Service account Firebase оставляйте на сервере. В приложение его класть нельзя.
|
|
89
99
|
|
|
@@ -100,19 +110,26 @@ apply plugin: "com.google.gms.google-services"
|
|
|
100
110
|
|
|
101
111
|
3. Добавьте classpath плагина в `android/build.gradle` / `settings.gradle` по [инструкции Firebase для Android](https://firebase.google.com/docs/android/setup).
|
|
102
112
|
|
|
103
|
-
Без `initialize(...)` или этого файла `getCredentials()` бросит ошибку.
|
|
113
|
+
Без `initialize(...)` или этого файла `getCredentials()` бросит ошибку.
|
|
104
114
|
|
|
105
115
|
Если в хост-приложении уже есть свой `FirebaseMessagingService`, обработать `com.google.firebase.MESSAGING_EVENT` может только один сервис. Оставьте сервис этой библиотеки или пробрасывайте события в него.
|
|
106
116
|
|
|
107
117
|
## Входящее сообщение и тап
|
|
108
118
|
|
|
109
|
-
|
|
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`.
|
|
110
125
|
- `onNotificationPress` — пользователь открыл уведомление, в том числе при холодном старте.
|
|
111
126
|
- На iOS видимый пуш в фоне или при убитом приложении приходит только в тап, не в `onMessage`. Это ограничение ОС.
|
|
112
127
|
|
|
128
|
+
Если `onMessage` бросил ошибку или не ответил примерно за 2 секунды, баннер не показывается.
|
|
129
|
+
|
|
113
130
|
## Web
|
|
114
131
|
|
|
115
|
-
|
|
132
|
+
`initialize()` резолвится. `getCredentials()` бросает ошибку. Слушатели (`onMessage`, `onNotificationPress`) ничего не делают.
|
|
116
133
|
|
|
117
134
|
## Contributing
|
|
118
135
|
|
|
@@ -5,20 +5,14 @@ import com.margelo.nitro.core.Promise
|
|
|
5
5
|
|
|
6
6
|
@DoNotStrip
|
|
7
7
|
class PushSignal : HybridPushSignalSpec() {
|
|
8
|
-
override fun initialize(config: AndroidFirebaseConfig) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
override fun requestPermission(): Promise<PermissionStatus> {
|
|
19
|
-
val promise = Promise<PermissionStatus>()
|
|
20
|
-
PushSignalCenter.requestPermission { status ->
|
|
21
|
-
promise.resolve(status)
|
|
8
|
+
override fun initialize(config: AndroidFirebaseConfig): Promise<Unit> {
|
|
9
|
+
val promise = Promise<Unit>()
|
|
10
|
+
PushSignalCenter.initialize(config) { error ->
|
|
11
|
+
if (error == null) {
|
|
12
|
+
promise.resolve(Unit)
|
|
13
|
+
} else {
|
|
14
|
+
promise.reject(error)
|
|
15
|
+
}
|
|
22
16
|
}
|
|
23
17
|
return promise
|
|
24
18
|
}
|
|
@@ -27,14 +21,14 @@ class PushSignal : HybridPushSignalSpec() {
|
|
|
27
21
|
return Promise.async {
|
|
28
22
|
val token = PushSignalCenter.fetchToken()
|
|
29
23
|
PushCredentials(
|
|
30
|
-
PushPlatform.
|
|
24
|
+
PushPlatform.ANDROID_OS,
|
|
31
25
|
token,
|
|
32
26
|
null
|
|
33
27
|
)
|
|
34
28
|
}
|
|
35
29
|
}
|
|
36
30
|
|
|
37
|
-
override fun setOnMessage(callback: (message: PushMessage) ->
|
|
31
|
+
override fun setOnMessage(callback: (message: PushMessage) -> Promise<Promise<Boolean>>) {
|
|
38
32
|
PushSignalCenter.setOnMessage(callback)
|
|
39
33
|
}
|
|
40
34
|
|
|
@@ -1,43 +1,45 @@
|
|
|
1
1
|
package com.margelo.nitro.pushsignal
|
|
2
2
|
|
|
3
|
-
import android.Manifest
|
|
4
3
|
import android.app.Activity
|
|
5
4
|
import android.app.Application
|
|
5
|
+
import android.app.NotificationChannel
|
|
6
|
+
import android.app.NotificationManager
|
|
7
|
+
import android.app.PendingIntent
|
|
6
8
|
import android.content.Context
|
|
7
9
|
import android.content.Intent
|
|
8
|
-
import android.content.pm.PackageManager
|
|
9
10
|
import android.os.Build
|
|
10
11
|
import android.os.Bundle
|
|
12
|
+
import android.os.Handler
|
|
13
|
+
import android.os.Looper
|
|
11
14
|
import androidx.activity.ComponentActivity
|
|
12
|
-
import androidx.
|
|
13
|
-
import androidx.activity.result.contract.ActivityResultContracts
|
|
14
|
-
import androidx.core.app.NotificationManagerCompat
|
|
15
|
-
import androidx.core.content.ContextCompat
|
|
15
|
+
import androidx.core.app.NotificationCompat
|
|
16
16
|
import com.google.firebase.FirebaseApp
|
|
17
17
|
import com.google.firebase.FirebaseOptions
|
|
18
18
|
import com.google.firebase.messaging.FirebaseMessaging
|
|
19
19
|
import com.google.android.gms.tasks.Tasks
|
|
20
20
|
import com.google.firebase.messaging.RemoteMessage
|
|
21
|
+
import com.margelo.nitro.core.Promise
|
|
21
22
|
import java.util.Collections
|
|
23
|
+
import java.util.UUID
|
|
22
24
|
import java.util.WeakHashMap
|
|
23
25
|
import java.util.concurrent.CopyOnWriteArrayList
|
|
26
|
+
import java.util.concurrent.atomic.AtomicBoolean
|
|
24
27
|
|
|
25
28
|
internal object PushSignalCenter : Application.ActivityLifecycleCallbacks {
|
|
26
|
-
private const val PREFS_NAME = "pushsignal"
|
|
27
|
-
private const val PREF_ASKED_PERMISSION = "asked_notifications"
|
|
28
29
|
private const val EXTRA_HANDLED = "pushsignal.handled"
|
|
29
|
-
private const val
|
|
30
|
+
private const val CHANNEL_ID = "push_signal_default"
|
|
31
|
+
private const val FOREGROUND_TIMEOUT_MS = 2_000L
|
|
30
32
|
|
|
31
33
|
private val lock = Any()
|
|
34
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
32
35
|
@Volatile private var application: Application? = null
|
|
33
36
|
@Volatile private var currentActivity: Activity? = null
|
|
34
|
-
@Volatile private var
|
|
35
|
-
@Volatile private var onMessage: ((PushMessage) -> Unit)? = null
|
|
37
|
+
@Volatile private var onMessage: ((PushMessage) -> Promise<Promise<Boolean>>)? = null
|
|
36
38
|
@Volatile private var onNotificationPress: ((PushMessage) -> Unit)? = null
|
|
37
39
|
@Volatile private var pendingPress: PushMessage? = null
|
|
38
|
-
private val permissionWaiters = CopyOnWriteArrayList<(PermissionStatus) -> Unit>()
|
|
39
40
|
private val registeredActivities = Collections.newSetFromMap(WeakHashMap<Activity, Boolean>())
|
|
40
41
|
@Volatile private var pendingFirebaseConfig: AndroidFirebaseConfig? = null
|
|
42
|
+
private val initializeWaiters = CopyOnWriteArrayList<(Exception?) -> Unit>()
|
|
41
43
|
|
|
42
44
|
fun attach(context: Context) {
|
|
43
45
|
val app = context.applicationContext as? Application ?: return
|
|
@@ -54,25 +56,27 @@ internal object PushSignalCenter : Application.ActivityLifecycleCallbacks {
|
|
|
54
56
|
}
|
|
55
57
|
pendingFirebaseConfig?.let { config ->
|
|
56
58
|
pendingFirebaseConfig = null
|
|
57
|
-
applyFirebaseConfig(app, config)
|
|
59
|
+
finishInitialize(applyFirebaseConfig(app, config))
|
|
58
60
|
}
|
|
59
61
|
}
|
|
60
62
|
|
|
61
|
-
fun initialize(config: AndroidFirebaseConfig) {
|
|
63
|
+
fun initialize(config: AndroidFirebaseConfig, onDone: (Exception?) -> Unit) {
|
|
62
64
|
if (!config.hasRequiredFields()) {
|
|
65
|
+
onDone(null)
|
|
63
66
|
return
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
val context = application
|
|
67
70
|
if (context == null) {
|
|
68
71
|
pendingFirebaseConfig = config
|
|
72
|
+
initializeWaiters.add(onDone)
|
|
69
73
|
return
|
|
70
74
|
}
|
|
71
75
|
|
|
72
|
-
applyFirebaseConfig(context, config)
|
|
76
|
+
onDone(applyFirebaseConfig(context, config))
|
|
73
77
|
}
|
|
74
78
|
|
|
75
|
-
fun setOnMessage(callback: (PushMessage) ->
|
|
79
|
+
fun setOnMessage(callback: (PushMessage) -> Promise<Promise<Boolean>>) {
|
|
76
80
|
onMessage = callback
|
|
77
81
|
}
|
|
78
82
|
|
|
@@ -88,60 +92,6 @@ internal object PushSignalCenter : Application.ActivityLifecycleCallbacks {
|
|
|
88
92
|
}
|
|
89
93
|
}
|
|
90
94
|
|
|
91
|
-
fun getPermissionStatus(): PermissionStatus {
|
|
92
|
-
val context = application ?: return PermissionStatus.NOTDETERMINED
|
|
93
|
-
val notificationsEnabled = NotificationManagerCompat.from(context).areNotificationsEnabled()
|
|
94
|
-
|
|
95
|
-
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
|
96
|
-
return if (notificationsEnabled) {
|
|
97
|
-
PermissionStatus.AUTHORIZED
|
|
98
|
-
} else {
|
|
99
|
-
PermissionStatus.DENIED
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
val granted =
|
|
104
|
-
ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) ==
|
|
105
|
-
PackageManager.PERMISSION_GRANTED
|
|
106
|
-
if (granted && notificationsEnabled) {
|
|
107
|
-
return PermissionStatus.AUTHORIZED
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
val asked =
|
|
111
|
-
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
112
|
-
.getBoolean(PREF_ASKED_PERMISSION, false)
|
|
113
|
-
return if (!asked && !granted) {
|
|
114
|
-
PermissionStatus.NOTDETERMINED
|
|
115
|
-
} else {
|
|
116
|
-
PermissionStatus.DENIED
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
fun requestPermission(onResult: (PermissionStatus) -> Unit) {
|
|
121
|
-
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
|
122
|
-
onResult(getPermissionStatus())
|
|
123
|
-
return
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
val activity = currentActivity
|
|
127
|
-
val launcher = permissionLauncher
|
|
128
|
-
if (activity == null || launcher == null) {
|
|
129
|
-
onResult(getPermissionStatus())
|
|
130
|
-
return
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (
|
|
134
|
-
ContextCompat.checkSelfPermission(activity, Manifest.permission.POST_NOTIFICATIONS) ==
|
|
135
|
-
PackageManager.PERMISSION_GRANTED
|
|
136
|
-
) {
|
|
137
|
-
onResult(getPermissionStatus())
|
|
138
|
-
return
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
permissionWaiters.add(onResult)
|
|
142
|
-
launcher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
|
143
|
-
}
|
|
144
|
-
|
|
145
95
|
fun fetchToken(): String {
|
|
146
96
|
val context = application
|
|
147
97
|
?: throw IllegalStateException("PushSignal is not initialized")
|
|
@@ -153,7 +103,7 @@ internal object PushSignalCenter : Application.ActivityLifecycleCallbacks {
|
|
|
153
103
|
FirebaseApp.getInstance()
|
|
154
104
|
} catch (error: IllegalStateException) {
|
|
155
105
|
throw IllegalStateException(
|
|
156
|
-
"Firebase is not configured. Call initialize({
|
|
106
|
+
"Firebase is not configured. Call initialize({ project_id, mobilesdk_app_id, current_key, project_number }) or add google-services.json.",
|
|
157
107
|
error
|
|
158
108
|
)
|
|
159
109
|
}
|
|
@@ -163,7 +113,7 @@ internal object PushSignalCenter : Application.ActivityLifecycleCallbacks {
|
|
|
163
113
|
Tasks.await(task)
|
|
164
114
|
} catch (error: Exception) {
|
|
165
115
|
throw IllegalStateException(
|
|
166
|
-
"Failed to get an FCM token. Call initialize({
|
|
116
|
+
"Failed to get an FCM token. Call initialize({ project_id, mobilesdk_app_id, current_key, project_number }) or add google-services.json.",
|
|
167
117
|
error
|
|
168
118
|
)
|
|
169
119
|
}
|
|
@@ -175,12 +125,117 @@ internal object PushSignalCenter : Application.ActivityLifecycleCallbacks {
|
|
|
175
125
|
return token
|
|
176
126
|
}
|
|
177
127
|
|
|
178
|
-
fun emitMessage(
|
|
179
|
-
|
|
128
|
+
fun emitMessage(remoteMessage: RemoteMessage) {
|
|
129
|
+
val message = remoteMessage.toPushMessage()
|
|
130
|
+
val inForeground = currentActivity != null
|
|
131
|
+
resolveShouldShowBanner(message) { shouldShow ->
|
|
132
|
+
if (
|
|
133
|
+
shouldShow &&
|
|
134
|
+
inForeground &&
|
|
135
|
+
(!message.title.isNullOrEmpty() || !message.body.isNullOrEmpty())
|
|
136
|
+
) {
|
|
137
|
+
postForegroundNotification(message)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
180
140
|
}
|
|
181
141
|
|
|
182
|
-
fun
|
|
183
|
-
|
|
142
|
+
private fun resolveShouldShowBanner(
|
|
143
|
+
message: PushMessage,
|
|
144
|
+
onDone: (Boolean) -> Unit
|
|
145
|
+
) {
|
|
146
|
+
val callback = onMessage
|
|
147
|
+
if (callback == null) {
|
|
148
|
+
onDone(false)
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
val delivered = AtomicBoolean(false)
|
|
153
|
+
val timeout = Runnable {
|
|
154
|
+
if (delivered.compareAndSet(false, true)) {
|
|
155
|
+
onDone(false)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
mainHandler.postDelayed(timeout, FOREGROUND_TIMEOUT_MS)
|
|
159
|
+
|
|
160
|
+
val finish = { shouldShow: Boolean ->
|
|
161
|
+
if (delivered.compareAndSet(false, true)) {
|
|
162
|
+
mainHandler.removeCallbacks(timeout)
|
|
163
|
+
onDone(shouldShow)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
callback(message)
|
|
169
|
+
.then { inner ->
|
|
170
|
+
inner
|
|
171
|
+
.then { shouldShow -> finish(shouldShow) }
|
|
172
|
+
.catch { finish(false) }
|
|
173
|
+
}
|
|
174
|
+
.catch { finish(false) }
|
|
175
|
+
} catch (_: Throwable) {
|
|
176
|
+
finish(false)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private fun postForegroundNotification(message: PushMessage) {
|
|
181
|
+
val context = application ?: return
|
|
182
|
+
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
|
|
183
|
+
?: return
|
|
184
|
+
|
|
185
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
186
|
+
val existing = manager.getNotificationChannel(CHANNEL_ID)
|
|
187
|
+
if (existing == null) {
|
|
188
|
+
manager.createNotificationChannel(
|
|
189
|
+
NotificationChannel(
|
|
190
|
+
CHANNEL_ID,
|
|
191
|
+
"Notifications",
|
|
192
|
+
NotificationManager.IMPORTANCE_HIGH
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
|
|
199
|
+
?: return
|
|
200
|
+
launchIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
|
201
|
+
launchIntent.putExtra("google.message_id", message.id ?: UUID.randomUUID().toString())
|
|
202
|
+
message.title?.let { launchIntent.putExtra("gcm.notification.title", it) }
|
|
203
|
+
message.body?.let { launchIntent.putExtra("gcm.notification.body", it) }
|
|
204
|
+
message.data.forEach { (key, value) ->
|
|
205
|
+
launchIntent.putExtra(key, value)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
val requestCode = (message.id ?: message.title ?: "push").hashCode()
|
|
209
|
+
val pendingIntent = PendingIntent.getActivity(
|
|
210
|
+
context,
|
|
211
|
+
requestCode,
|
|
212
|
+
launchIntent,
|
|
213
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
|
|
217
|
+
.setSmallIcon(smallIcon(context))
|
|
218
|
+
.setContentTitle(message.title.orEmpty())
|
|
219
|
+
.setContentText(message.body.orEmpty())
|
|
220
|
+
.setContentIntent(pendingIntent)
|
|
221
|
+
.setAutoCancel(true)
|
|
222
|
+
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
|
223
|
+
.setDefaults(NotificationCompat.DEFAULT_SOUND)
|
|
224
|
+
.setNumber(1)
|
|
225
|
+
|
|
226
|
+
manager.notify(requestCode, builder.build())
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private fun smallIcon(context: Context): Int {
|
|
230
|
+
val named = context.resources.getIdentifier("ic_notification", "drawable", context.packageName)
|
|
231
|
+
if (named != 0) {
|
|
232
|
+
return named
|
|
233
|
+
}
|
|
234
|
+
val appIcon = context.applicationInfo.icon
|
|
235
|
+
if (appIcon != 0) {
|
|
236
|
+
return appIcon
|
|
237
|
+
}
|
|
238
|
+
return android.R.drawable.stat_notify_more
|
|
184
239
|
}
|
|
185
240
|
|
|
186
241
|
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
|
@@ -209,7 +264,6 @@ internal object PushSignalCenter : Application.ActivityLifecycleCallbacks {
|
|
|
209
264
|
override fun onActivityDestroyed(activity: Activity) {
|
|
210
265
|
if (currentActivity === activity) {
|
|
211
266
|
currentActivity = null
|
|
212
|
-
permissionLauncher = null
|
|
213
267
|
}
|
|
214
268
|
}
|
|
215
269
|
|
|
@@ -219,24 +273,6 @@ internal object PushSignalCenter : Application.ActivityLifecycleCallbacks {
|
|
|
219
273
|
}
|
|
220
274
|
|
|
221
275
|
val componentActivity = activity as? ComponentActivity ?: return
|
|
222
|
-
|
|
223
|
-
permissionLauncher =
|
|
224
|
-
componentActivity.activityResultRegistry.register(
|
|
225
|
-
PERMISSION_LAUNCHER_KEY,
|
|
226
|
-
componentActivity,
|
|
227
|
-
ActivityResultContracts.RequestPermission()
|
|
228
|
-
) {
|
|
229
|
-
activity
|
|
230
|
-
.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
231
|
-
.edit()
|
|
232
|
-
.putBoolean(PREF_ASKED_PERMISSION, true)
|
|
233
|
-
.apply()
|
|
234
|
-
val status = getPermissionStatus()
|
|
235
|
-
val waiters = permissionWaiters.toList()
|
|
236
|
-
permissionWaiters.clear()
|
|
237
|
-
waiters.forEach { it(status) }
|
|
238
|
-
}
|
|
239
|
-
|
|
240
276
|
componentActivity.addOnNewIntentListener { intent ->
|
|
241
277
|
handleIntent(intent)
|
|
242
278
|
}
|
|
@@ -251,25 +287,36 @@ internal object PushSignalCenter : Application.ActivityLifecycleCallbacks {
|
|
|
251
287
|
emitPress(intent.toPushMessage())
|
|
252
288
|
}
|
|
253
289
|
|
|
254
|
-
private fun applyFirebaseConfig(context: Context, config: AndroidFirebaseConfig) {
|
|
255
|
-
if (
|
|
256
|
-
return
|
|
290
|
+
private fun applyFirebaseConfig(context: Context, config: AndroidFirebaseConfig): Exception? {
|
|
291
|
+
if (FirebaseApp.getApps(context).isNotEmpty()) {
|
|
292
|
+
return null
|
|
257
293
|
}
|
|
258
294
|
|
|
259
|
-
|
|
295
|
+
if (!config.hasRequiredFields()) {
|
|
296
|
+
return null
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return try {
|
|
260
300
|
val options =
|
|
261
301
|
FirebaseOptions.Builder()
|
|
262
|
-
.setProjectId(config.
|
|
263
|
-
.setApplicationId(config.
|
|
264
|
-
.setApiKey(config.
|
|
265
|
-
.setGcmSenderId(config.
|
|
302
|
+
.setProjectId(config.project_id!!.trim())
|
|
303
|
+
.setApplicationId(config.mobilesdk_app_id!!.trim())
|
|
304
|
+
.setApiKey(config.current_key!!.trim())
|
|
305
|
+
.setGcmSenderId(config.project_number!!.trim())
|
|
266
306
|
.build()
|
|
267
307
|
FirebaseApp.initializeApp(context, options)
|
|
268
|
-
|
|
269
|
-
|
|
308
|
+
null
|
|
309
|
+
} catch (error: Exception) {
|
|
310
|
+
error
|
|
270
311
|
}
|
|
271
312
|
}
|
|
272
313
|
|
|
314
|
+
private fun finishInitialize(error: Exception?) {
|
|
315
|
+
val waiters = initializeWaiters.toList()
|
|
316
|
+
initializeWaiters.clear()
|
|
317
|
+
waiters.forEach { it(error) }
|
|
318
|
+
}
|
|
319
|
+
|
|
273
320
|
private fun emitPress(message: PushMessage) {
|
|
274
321
|
val listener = onNotificationPress
|
|
275
322
|
if (listener != null) {
|
|
@@ -335,10 +382,10 @@ private fun Intent.toPushMessage(): PushMessage {
|
|
|
335
382
|
}
|
|
336
383
|
|
|
337
384
|
private fun AndroidFirebaseConfig.hasRequiredFields(): Boolean {
|
|
338
|
-
return !
|
|
339
|
-
!
|
|
340
|
-
!
|
|
341
|
-
!
|
|
385
|
+
return !project_id.isNullOrBlank() &&
|
|
386
|
+
!mobilesdk_app_id.isNullOrBlank() &&
|
|
387
|
+
!current_key.isNullOrBlank() &&
|
|
388
|
+
!project_number.isNullOrBlank()
|
|
342
389
|
}
|
|
343
390
|
|
|
344
391
|
private const val EXTRA_HANDLED_KEY = "pushsignal.handled"
|
package/ios/PushSignal.swift
CHANGED
|
@@ -6,19 +6,10 @@ class PushSignal: HybridPushSignalSpec {
|
|
|
6
6
|
PushSignalCenter.shared.install()
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
func initialize(config: AndroidFirebaseConfig) throws {
|
|
9
|
+
func initialize(config: AndroidFirebaseConfig) throws -> Promise<Void> {
|
|
10
10
|
_ = config
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
func getPermissionStatus() throws -> Promise<PermissionStatus> {
|
|
14
|
-
return Promise.async {
|
|
15
|
-
await PushSignalCenter.shared.permissionStatus()
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
func requestPermission() throws -> Promise<PermissionStatus> {
|
|
20
11
|
return Promise.async {
|
|
21
|
-
|
|
12
|
+
()
|
|
22
13
|
}
|
|
23
14
|
}
|
|
24
15
|
|
|
@@ -28,7 +19,7 @@ class PushSignal: HybridPushSignalSpec {
|
|
|
28
19
|
}
|
|
29
20
|
}
|
|
30
21
|
|
|
31
|
-
func setOnMessage(callback: @escaping (PushMessage) ->
|
|
22
|
+
func setOnMessage(callback: @escaping (PushMessage) -> Promise<Promise<Bool>>) throws {
|
|
32
23
|
PushSignalCenter.shared.onMessage = callback
|
|
33
24
|
}
|
|
34
25
|
|