react-native-push-signal 0.1.8 → 0.1.9

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.
@@ -11,7 +11,7 @@ Pod::Spec.new do |s|
11
11
  s.authors = package["author"]
12
12
 
13
13
  s.platforms = { :ios => min_ios_version_supported }
14
- s.source = { :git => "https://antonseagull.com.git", :tag => "#{s.version}" }
14
+ s.source = { :git => "https://github.com/AntonSeagull/react-native-push-signal.git", :tag => "#{s.version}" }
15
15
 
16
16
  s.source_files = "ios/**/*.{h,m,mm,cpp}"
17
17
  s.private_header_files = "ios/**/*.h"
package/README.md CHANGED
@@ -108,6 +108,23 @@ Without `initialize(...)` or that file, `getCredentials()` throws.
108
108
 
109
109
  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.
110
110
 
111
+ ### When it fails on Xiaomi: `SERVICE_NOT_AVAILABLE`
112
+
113
+ If `getCredentials()` throws `Failed to get an FCM token: SERVICE_NOT_AVAILABLE`, the FCM token could not be obtained. Your server never receives a token, so there is nowhere to send pushes — delivery itself is not the problem.
114
+
115
+ Before requesting the token the library checks Google Play services and retries a few times with backoff, but some causes can only be fixed on the device:
116
+
117
+ - **Google account.** The device must be signed into a Google account. FCM does not issue a token without one.
118
+ - **Google Play services.** They must be installed, enabled and up to date. On a China-only ROM without GMS, FCM cannot work at all — you need another push provider.
119
+ - **Network.** Check connectivity and disable VPN and private DNS.
120
+ - **MIUI / HyperOS.** Enable autostart and remove battery restrictions: Settings → Apps → your app → Autostart; Battery → No restrictions. Allow background data for the app.
121
+
122
+ What to look for in the logs:
123
+
124
+ ```sh
125
+ adb logcat | grep -iE "SERVICE_NOT_AVAILABLE|FirebaseMessaging|PushSignal"
126
+ ```
127
+
111
128
  ## Incoming messages vs taps
112
129
 
113
130
  | App state | iOS | Android (notification payload) | Android (data-only) |
package/README.ru.md CHANGED
@@ -108,6 +108,23 @@ apply plugin: "com.google.gms.google-services"
108
108
 
109
109
  Если в хост-приложении уже есть свой `FirebaseMessagingService`, обработать `com.google.firebase.MESSAGING_EVENT` может только один сервис. Оставьте сервис этой библиотеки или пробрасывайте события в него.
110
110
 
111
+ ### Если не работает на Xiaomi: `SERVICE_NOT_AVAILABLE`
112
+
113
+ Если `getCredentials()` бросает `Failed to get an FCM token: SERVICE_NOT_AVAILABLE`, значит не удалось получить FCM-токен. Сервер не получает токен и слать пуши некуда — сама доставка здесь ни при чём.
114
+
115
+ Перед запросом токена библиотека проверяет Google Play services и делает несколько попыток с паузами, но часть причин лечится только на устройстве:
116
+
117
+ - **Google-аккаунт.** На устройстве должен быть выполнен вход в Google-аккаунт. Без него FCM токен не выдаёт.
118
+ - **Google Play services.** Должны быть установлены, включены и обновлены. На китайской прошивке без GMS FCM не заработает в принципе — нужен другой провайдер пушей.
119
+ - **Сеть.** Проверьте интернет, отключите VPN и приватный DNS.
120
+ - **MIUI / HyperOS.** Включите автозапуск и снимите ограничения батареи: Настройки → Приложения → «ваше приложение» → Автозапуск; Батарея → Без ограничений. Разрешите приложению фоновые данные.
121
+
122
+ Что смотреть в логах:
123
+
124
+ ```sh
125
+ adb logcat | grep -iE "SERVICE_NOT_AVAILABLE|FirebaseMessaging|PushSignal"
126
+ ```
127
+
111
128
  ## Входящее сообщение и тап
112
129
 
113
130
  | Состояние | iOS | Android (notification payload) | Android (data-only) |
@@ -51,5 +51,6 @@ dependencies {
51
51
  implementation "com.facebook.react:react-android"
52
52
  implementation "androidx.activity:activity-ktx:1.10.1"
53
53
  implementation "androidx.core:core-ktx:1.16.0"
54
+ implementation "com.google.android.gms:play-services-base:18.5.0"
54
55
  implementation "com.google.firebase:firebase-messaging:24.1.2"
55
56
  }
@@ -11,8 +11,11 @@ import android.os.Build
11
11
  import android.os.Bundle
12
12
  import android.os.Handler
13
13
  import android.os.Looper
14
+ import android.util.Log
14
15
  import androidx.activity.ComponentActivity
15
16
  import androidx.core.app.NotificationCompat
17
+ import com.google.android.gms.common.ConnectionResult
18
+ import com.google.android.gms.common.GoogleApiAvailability
16
19
  import com.google.android.gms.tasks.Tasks
17
20
  import com.google.firebase.FirebaseApp
18
21
  import com.google.firebase.FirebaseOptions
@@ -22,10 +25,16 @@ import java.util.Collections
22
25
  import java.util.UUID
23
26
  import java.util.WeakHashMap
24
27
  import java.util.concurrent.CopyOnWriteArrayList
28
+ import java.util.concurrent.TimeUnit
25
29
 
26
30
  internal object PushSignalCenter : Application.ActivityLifecycleCallbacks {
31
+ private const val TAG = "PushSignal"
27
32
  private const val EXTRA_HANDLED = "pushsignal.handled"
28
33
  private const val CHANNEL_ID = "push_signal_default"
34
+ private const val TOKEN_TIMEOUT_SECONDS = 10L
35
+ private const val TOKEN_MAX_ATTEMPTS = 5
36
+ private const val TOKEN_INITIAL_RETRY_DELAY_MS = 1_000L
37
+ private const val TOKEN_MAX_RETRY_DELAY_MS = 8_000L
29
38
 
30
39
  private val lock = Any()
31
40
  private val mainHandler = Handler(Looper.getMainLooper())
@@ -100,6 +109,8 @@ internal object PushSignalCenter : Application.ActivityLifecycleCallbacks {
100
109
  val context = application
101
110
  ?: throw IllegalStateException("PushSignal is not initialized")
102
111
 
112
+ ensurePlayServices(context)
113
+
103
114
  try {
104
115
  if (FirebaseApp.getApps(context).isEmpty()) {
105
116
  FirebaseApp.initializeApp(context)
@@ -112,15 +123,7 @@ internal object PushSignalCenter : Application.ActivityLifecycleCallbacks {
112
123
  )
113
124
  }
114
125
 
115
- val task = FirebaseMessaging.getInstance().token
116
- val token = try {
117
- Tasks.await(task)
118
- } catch (error: Exception) {
119
- throw IllegalStateException(
120
- "Failed to get an FCM token. Call initialize({ project_id, mobilesdk_app_id, current_key, project_number }) or add google-services.json.",
121
- error
122
- )
123
- }
126
+ val token = awaitTokenWithRetry()
124
127
 
125
128
  if (token.isNullOrEmpty()) {
126
129
  throw IllegalStateException("Firebase returned an empty FCM token")
@@ -129,6 +132,93 @@ internal object PushSignalCenter : Application.ActivityLifecycleCallbacks {
129
132
  return token
130
133
  }
131
134
 
135
+ /**
136
+ * Fails fast with an actionable message when Google Play services cannot serve FCM
137
+ * (for example a China-only ROM without GMS).
138
+ */
139
+ private fun ensurePlayServices(context: Context) {
140
+ val status = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context)
141
+ if (status == ConnectionResult.SUCCESS) {
142
+ return
143
+ }
144
+
145
+ val reason = when (status) {
146
+ ConnectionResult.SERVICE_MISSING ->
147
+ "Google Play services are missing on this device, so FCM cannot be used. A ROM without GMS needs another push provider."
148
+ ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED ->
149
+ "Google Play services are outdated. Update them in the Play Store and try again."
150
+ ConnectionResult.SERVICE_DISABLED ->
151
+ "Google Play services are disabled. Enable them in the device settings and try again."
152
+ ConnectionResult.SERVICE_INVALID ->
153
+ "Google Play services are invalid or corrupted on this device. Reinstalling them usually helps."
154
+ else ->
155
+ "Google Play services are unavailable (code $status)."
156
+ }
157
+ Log.w(TAG, "Play services check failed: $reason")
158
+ throw IllegalStateException(reason)
159
+ }
160
+
161
+ /**
162
+ * FCM returns SERVICE_NOT_AVAILABLE for transient conditions (no Google account yet,
163
+ * Play services still starting, flaky network). Xiaomi/MIUI devices hit this often,
164
+ * so retry with backoff before surfacing the failure.
165
+ */
166
+ private fun awaitTokenWithRetry(): String? {
167
+ var delayMs = TOKEN_INITIAL_RETRY_DELAY_MS
168
+ var lastError: Exception? = null
169
+
170
+ for (attempt in 1..TOKEN_MAX_ATTEMPTS) {
171
+ try {
172
+ return Tasks.await(
173
+ FirebaseMessaging.getInstance().token,
174
+ TOKEN_TIMEOUT_SECONDS,
175
+ TimeUnit.SECONDS
176
+ )
177
+ } catch (error: Exception) {
178
+ lastError = error
179
+ if (attempt == TOKEN_MAX_ATTEMPTS || !isRetryableTokenError(error)) {
180
+ break
181
+ }
182
+ Log.w(TAG, "FCM token attempt $attempt/$TOKEN_MAX_ATTEMPTS failed: ${error.message}. Retrying in ${delayMs}ms")
183
+ try {
184
+ Thread.sleep(delayMs)
185
+ } catch (_: InterruptedException) {
186
+ Thread.currentThread().interrupt()
187
+ break
188
+ }
189
+ delayMs = (delayMs * 2).coerceAtMost(TOKEN_MAX_RETRY_DELAY_MS)
190
+ }
191
+ }
192
+
193
+ val cause = lastError
194
+ throw IllegalStateException(
195
+ "Failed to get an FCM token: ${cause?.message ?: "unknown error"}. " +
196
+ "Make sure the device is signed into a Google account, Google Play services are up to date, " +
197
+ "and the app is allowed to use background data. " +
198
+ "Call initialize({ project_id, mobilesdk_app_id, current_key, project_number }) or add google-services.json.",
199
+ cause
200
+ )
201
+ }
202
+
203
+ private fun isRetryableTokenError(error: Throwable): Boolean {
204
+ var current: Throwable? = error
205
+ while (current != null) {
206
+ if (current is java.io.IOException || current is java.util.concurrent.TimeoutException) {
207
+ return true
208
+ }
209
+ val message = current.message?.uppercase() ?: ""
210
+ if (
211
+ message.contains("SERVICE_NOT_AVAILABLE") ||
212
+ message.contains("TIMEOUT") ||
213
+ message.contains("INTERNAL_SERVER_ERROR")
214
+ ) {
215
+ return true
216
+ }
217
+ current = current.cause
218
+ }
219
+ return false
220
+ }
221
+
132
222
  fun emitMessage(remoteMessage: RemoteMessage) {
133
223
  val message = remoteMessage.toPushMessage()
134
224
  runOnMain { deliverMessage(message) }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "react-native-push-signal",
3
- "version": "0.1.8",
4
- "description": "Push",
3
+ "version": "0.1.9",
4
+ "description": "React Native push notifications with direct APNs and FCM integration — get device tokens for your server and listen for incoming messages and taps",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
7
7
  "exports": {
@@ -45,18 +45,28 @@
45
45
  "keywords": [
46
46
  "react-native",
47
47
  "ios",
48
- "android"
48
+ "android",
49
+ "push",
50
+ "push-notifications",
51
+ "notifications",
52
+ "apns",
53
+ "fcm",
54
+ "firebase",
55
+ "firebase-cloud-messaging",
56
+ "device-token",
57
+ "push-token",
58
+ "remote-notifications"
49
59
  ],
50
60
  "repository": {
51
61
  "type": "git",
52
- "url": "git+https://antonseagull.com.git"
62
+ "url": "git+https://github.com/AntonSeagull/react-native-push-signal.git"
53
63
  },
54
64
  "author": "AntonSeagull <hi@antonseagull.com> (https://antonseagull.com)",
55
65
  "license": "MIT",
56
66
  "bugs": {
57
- "url": "https://antonseagull.com/issues"
67
+ "url": "https://github.com/AntonSeagull/react-native-push-signal/issues"
58
68
  },
59
- "homepage": "https://antonseagull.com#readme",
69
+ "homepage": "https://github.com/AntonSeagull/react-native-push-signal#readme",
60
70
  "publishConfig": {
61
71
  "registry": "https://registry.npmjs.org/"
62
72
  },