react-native-push-signal 0.1.7 → 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.
- package/PushSignal.podspec +1 -1
- package/README.md +17 -0
- package/README.ru.md +17 -0
- package/android/build.gradle +1 -0
- package/android/src/main/java/com/pushsignal/PushSignalCenter.kt +99 -9
- package/ios/PushSignalCenter.m +8 -33
- package/package.json +16 -6
package/PushSignal.podspec
CHANGED
|
@@ -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://
|
|
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) |
|
package/android/build.gradle
CHANGED
|
@@ -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
|
|
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/ios/PushSignalCenter.m
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#import "PushSignalCenter.h"
|
|
2
2
|
#import "PushSignalLaunchStore.h"
|
|
3
|
-
#import <
|
|
3
|
+
#import <objc/runtime.h>
|
|
4
4
|
#import <UIKit/UIKit.h>
|
|
5
5
|
|
|
6
6
|
@interface PushSignalAppDelegateHook : NSObject
|
|
@@ -550,45 +550,20 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response
|
|
|
550
550
|
- (void)pushSignal_application:(UIApplication *)application
|
|
551
551
|
didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
|
|
552
552
|
[[PushSignalCenter shared] handleDeviceToken:deviceToken];
|
|
553
|
-
|
|
553
|
+
// After swizzle, this selector holds the original AppDelegate implementation (if any).
|
|
554
|
+
SEL original = @selector(pushSignal_application:didRegisterForRemoteNotificationsWithDeviceToken:);
|
|
555
|
+
if ([self respondsToSelector:original]) {
|
|
556
|
+
[self pushSignal_application:application didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
|
|
557
|
+
}
|
|
554
558
|
}
|
|
555
559
|
|
|
556
560
|
- (void)pushSignal_application:(UIApplication *)application
|
|
557
561
|
didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
|
|
558
562
|
[[PushSignalCenter shared] handleRegistrationError:error];
|
|
559
|
-
[self pushSignal_forwardError:application error:error];
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
- (void)pushSignal_forwardDeviceToken:(UIApplication *)application deviceToken:(NSData *)deviceToken {
|
|
563
|
-
SEL hooked = @selector(application:didRegisterForRemoteNotificationsWithDeviceToken:);
|
|
564
|
-
SEL original = @selector(pushSignal_application:didRegisterForRemoteNotificationsWithDeviceToken:);
|
|
565
|
-
if (![self respondsToSelector:original]) {
|
|
566
|
-
return;
|
|
567
|
-
}
|
|
568
|
-
Class cls = object_getClass(self);
|
|
569
|
-
IMP originalImp = class_getMethodImplementation(cls, original);
|
|
570
|
-
IMP hookedImp = class_getMethodImplementation(cls, hooked);
|
|
571
|
-
if (originalImp == NULL || originalImp == hookedImp) {
|
|
572
|
-
return;
|
|
573
|
-
}
|
|
574
|
-
void (*fn)(id, SEL, UIApplication *, NSData *) = (void (*)(id, SEL, UIApplication *, NSData *))originalImp;
|
|
575
|
-
fn(self, original, application, deviceToken);
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
- (void)pushSignal_forwardError:(UIApplication *)application error:(NSError *)error {
|
|
579
|
-
SEL hooked = @selector(application:didFailToRegisterForRemoteNotificationsWithError:);
|
|
580
563
|
SEL original = @selector(pushSignal_application:didFailToRegisterForRemoteNotificationsWithError:);
|
|
581
|
-
if (
|
|
582
|
-
|
|
583
|
-
}
|
|
584
|
-
Class cls = object_getClass(self);
|
|
585
|
-
IMP originalImp = class_getMethodImplementation(cls, original);
|
|
586
|
-
IMP hookedImp = class_getMethodImplementation(cls, hooked);
|
|
587
|
-
if (originalImp == NULL || originalImp == hookedImp) {
|
|
588
|
-
return;
|
|
564
|
+
if ([self respondsToSelector:original]) {
|
|
565
|
+
[self pushSignal_application:application didFailToRegisterForRemoteNotificationsWithError:error];
|
|
589
566
|
}
|
|
590
|
-
void (*fn)(id, SEL, UIApplication *, NSError *) = (void (*)(id, SEL, UIApplication *, NSError *))originalImp;
|
|
591
|
-
fn(self, original, application, error);
|
|
592
567
|
}
|
|
593
568
|
|
|
594
569
|
@end
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-push-signal",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
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://
|
|
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://
|
|
67
|
+
"url": "https://github.com/AntonSeagull/react-native-push-signal/issues"
|
|
58
68
|
},
|
|
59
|
-
"homepage": "https://
|
|
69
|
+
"homepage": "https://github.com/AntonSeagull/react-native-push-signal#readme",
|
|
60
70
|
"publishConfig": {
|
|
61
71
|
"registry": "https://registry.npmjs.org/"
|
|
62
72
|
},
|