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.
Files changed (55) hide show
  1. package/README.md +36 -19
  2. package/README.ru.md +36 -19
  3. package/android/src/main/java/com/margelo/nitro/pushsignal/PushSignal.kt +10 -16
  4. package/android/src/main/java/com/margelo/nitro/pushsignal/PushSignalCenter.kt +156 -109
  5. package/ios/PushSignal.swift +3 -12
  6. package/ios/PushSignalCenter.swift +36 -41
  7. package/lib/module/index.js +1 -1
  8. package/lib/module/index.js.map +1 -1
  9. package/lib/module/pushSignal.js +1 -7
  10. package/lib/module/pushSignal.js.map +1 -1
  11. package/lib/module/pushSignal.native.js +10 -9
  12. package/lib/module/pushSignal.native.js.map +1 -1
  13. package/lib/typescript/src/PushSignal.nitro.d.ts +9 -10
  14. package/lib/typescript/src/PushSignal.nitro.d.ts.map +1 -1
  15. package/lib/typescript/src/index.d.ts +2 -2
  16. package/lib/typescript/src/index.d.ts.map +1 -1
  17. package/lib/typescript/src/pushSignal.d.ts +3 -5
  18. package/lib/typescript/src/pushSignal.d.ts.map +1 -1
  19. package/lib/typescript/src/pushSignal.native.d.ts +3 -5
  20. package/lib/typescript/src/pushSignal.native.d.ts.map +1 -1
  21. package/nitrogen/generated/android/c++/JAndroidFirebaseConfig.hpp +16 -16
  22. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_bool_____PushMessage.hpp +128 -0
  23. package/nitrogen/generated/android/c++/JHybridPushSignalSpec.cpp +12 -35
  24. package/nitrogen/generated/android/c++/JHybridPushSignalSpec.hpp +2 -4
  25. package/nitrogen/generated/android/c++/JPushPlatform.hpp +3 -3
  26. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pushsignal/AndroidFirebaseConfig.kt +14 -14
  27. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pushsignal/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_bool_____PushMessage.kt +78 -0
  28. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pushsignal/HybridPushSignalSpec.kt +3 -11
  29. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pushsignal/PushPlatform.kt +1 -1
  30. package/nitrogen/generated/android/pushsignalOnLoad.cpp +2 -0
  31. package/nitrogen/generated/ios/PushSignal-Swift-Cxx-Bridge.cpp +30 -5
  32. package/nitrogen/generated/ios/PushSignal-Swift-Cxx-Bridge.hpp +140 -52
  33. package/nitrogen/generated/ios/PushSignal-Swift-Cxx-Umbrella.hpp +0 -3
  34. package/nitrogen/generated/ios/c++/HybridPushSignalSpecSwift.hpp +3 -20
  35. package/nitrogen/generated/ios/swift/AndroidFirebaseConfig.swift +17 -17
  36. package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_bool_____PushMessage.swift +61 -0
  37. package/nitrogen/generated/ios/swift/{Func_void_PermissionStatus.swift → Func_void.swift} +11 -11
  38. package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
  39. package/nitrogen/generated/ios/swift/Func_void_std__shared_ptr_Promise_bool__.swift +66 -0
  40. package/nitrogen/generated/ios/swift/HybridPushSignalSpec.swift +2 -4
  41. package/nitrogen/generated/ios/swift/HybridPushSignalSpec_cxx.swift +34 -43
  42. package/nitrogen/generated/ios/swift/PushPlatform.swift +4 -4
  43. package/nitrogen/generated/shared/c++/AndroidFirebaseConfig.hpp +17 -17
  44. package/nitrogen/generated/shared/c++/HybridPushSignalSpec.cpp +0 -2
  45. package/nitrogen/generated/shared/c++/HybridPushSignalSpec.hpp +3 -8
  46. package/nitrogen/generated/shared/c++/PushPlatform.hpp +4 -4
  47. package/package.json +2 -2
  48. package/src/PushSignal.nitro.ts +12 -10
  49. package/src/index.tsx +1 -3
  50. package/src/pushSignal.native.tsx +17 -17
  51. package/src/pushSignal.tsx +5 -13
  52. package/nitrogen/generated/android/c++/JPermissionStatus.hpp +0 -61
  53. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pushsignal/PermissionStatus.kt +0 -24
  54. package/nitrogen/generated/ios/swift/PermissionStatus.swift +0 -44
  55. 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 push permission, 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.
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
- projectId: 'my-project',
29
- applicationId: '1:123456789:android:abcd',
30
- apiKey: 'AIza...',
31
- gcmSenderId: '123456789',
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 getPermissionStatus();
35
- const afterRequest = await requestPermission();
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
- projectId: '...', // project_id
80
- applicationId: '...', // mobilesdk_app_id
81
- apiKey: '...', // current_key
82
- gcmSenderId: '...', // project_number
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` does nothing and does not throw. iOS ignores this call.
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. Notification permission (`POST_NOTIFICATIONS`) is requested on API 33+.
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
- - `onMessage` — the push arrived while the app is in the foreground (and Android data messages while the process is alive). The library does not draw a banner.
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
- Permission helpers resolve to `denied`. `getCredentials()` throws. Listeners are no-ops.
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
- Библиотека запрашивает разрешение на пуши, отдаёт токен устройства для вашего сервера и сообщает о входящем уведомлении или нажатии на него. Сами пуши она не отправляет — в APNs и FCM ходит бэкенд.
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
- projectId: 'my-project',
29
- applicationId: '1:123456789:android:abcd',
30
- apiKey: 'AIza...',
31
- gcmSenderId: '123456789',
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 getPermissionStatus();
35
- const afterRequest = await requestPermission();
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
- projectId: '...', // project_id
80
- applicationId: '...', // mobilesdk_app_id
81
- apiKey: '...', // current_key
82
- gcmSenderId: '...', // project_number
88
+ await initialize({
89
+ project_id: '...',
90
+ mobilesdk_app_id: '...',
91
+ current_key: '...',
92
+ project_number: '...',
83
93
  });
84
94
  ```
85
95
 
86
- Нужны все четыре поля. Если чего-то нет, `initialize` тихо ничего не делает и не бросает ошибку. На iOS вызов игнорируется.
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()` бросит ошибку. Разрешение на уведомления (`POST_NOTIFICATIONS`) запрашивается на API 33+.
113
+ Без `initialize(...)` или этого файла `getCredentials()` бросит ошибку.
104
114
 
105
115
  Если в хост-приложении уже есть свой `FirebaseMessagingService`, обработать `com.google.firebase.MESSAGING_EVENT` может только один сервис. Оставьте сервис этой библиотеки или пробрасывайте события в него.
106
116
 
107
117
  ## Входящее сообщение и тап
108
118
 
109
- - `onMessage` — пуш пришёл, пока приложение на переднем плане (и Android data-message, пока процесс жив). Баннер библиотека не рисует.
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
- Методы разрешения возвращают `denied`. `getCredentials()` бросает ошибку. Слушатели ничего не делают.
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
- PushSignalCenter.initialize(config)
10
- }
11
-
12
- override fun getPermissionStatus(): Promise<PermissionStatus> {
13
- return Promise.async {
14
- PushSignalCenter.getPermissionStatus()
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.ANDROID,
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) -> Unit) {
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.activity.result.ActivityResultLauncher
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 PERMISSION_LAUNCHER_KEY = "com.margelo.nitro.pushsignal.permission"
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 permissionLauncher: ActivityResultLauncher<String>? = null
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) -> Unit) {
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({ projectId, applicationId, apiKey, gcmSenderId }) or add google-services.json.",
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({ projectId, applicationId, apiKey, gcmSenderId }) or add google-services.json.",
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(message: PushMessage) {
179
- onMessage?.invoke(message)
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 emitMessage(remoteMessage: RemoteMessage) {
183
- emitMessage(remoteMessage.toPushMessage())
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 (!config.hasRequiredFields() || FirebaseApp.getApps(context).isNotEmpty()) {
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
- try {
295
+ if (!config.hasRequiredFields()) {
296
+ return null
297
+ }
298
+
299
+ return try {
260
300
  val options =
261
301
  FirebaseOptions.Builder()
262
- .setProjectId(config.projectId!!.trim())
263
- .setApplicationId(config.applicationId!!.trim())
264
- .setApiKey(config.apiKey!!.trim())
265
- .setGcmSenderId(config.gcmSenderId!!.trim())
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
- } catch (_: Exception) {
269
- // Keep initialize quiet; getCredentials reports a missing Firebase app.
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 !projectId.isNullOrBlank() &&
339
- !applicationId.isNullOrBlank() &&
340
- !apiKey.isNullOrBlank() &&
341
- !gcmSenderId.isNullOrBlank()
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"
@@ -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
- await PushSignalCenter.shared.requestPermission()
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) -> Void) throws {
22
+ func setOnMessage(callback: @escaping (PushMessage) -> Promise<Promise<Bool>>) throws {
32
23
  PushSignalCenter.shared.onMessage = callback
33
24
  }
34
25