capacitor-push-signal 0.0.2 → 0.0.4

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.
@@ -7,28 +7,61 @@ import android.app.NotificationManager
7
7
  import android.app.PendingIntent
8
8
  import android.content.Context
9
9
  import android.content.Intent
10
+ import android.content.pm.PackageManager
11
+ import android.graphics.Bitmap
12
+ import android.graphics.BitmapFactory
10
13
  import android.os.Build
11
14
  import android.os.Bundle
12
15
  import android.os.Handler
13
16
  import android.os.Looper
17
+ import android.util.Log
14
18
  import androidx.activity.ComponentActivity
15
19
  import androidx.core.app.NotificationCompat
20
+ import com.google.android.gms.common.ConnectionResult
21
+ import com.google.android.gms.common.GoogleApiAvailability
16
22
  import com.google.android.gms.tasks.Tasks
17
23
  import com.google.firebase.FirebaseApp
18
24
  import com.google.firebase.FirebaseOptions
19
25
  import com.google.firebase.messaging.FirebaseMessaging
20
26
  import com.google.firebase.messaging.RemoteMessage
27
+ import java.net.HttpURLConnection
28
+ import java.net.URL
21
29
  import java.util.Collections
22
30
  import java.util.UUID
23
31
  import java.util.WeakHashMap
24
32
  import java.util.concurrent.CopyOnWriteArrayList
33
+ import java.util.concurrent.Executors
34
+ import java.util.concurrent.TimeUnit
25
35
 
26
36
  internal object PushSignalCenter : Application.ActivityLifecycleCallbacks {
27
37
  private const val EXTRA_HANDLED = "pushsignal.handled"
28
38
  private const val CHANNEL_ID = "push_signal_default"
39
+ private const val IMAGE_CONNECT_TIMEOUT_MS = 8_000
40
+ private const val IMAGE_READ_TIMEOUT_MS = 8_000
41
+ private const val IMAGE_MAX_BYTES = 5L * 1024L * 1024L
42
+
43
+ private const val TOKEN_TIMEOUT_SECONDS = 10L
44
+ private const val TOKEN_MAX_ATTEMPTS = 5
45
+ private const val TOKEN_INITIAL_RETRY_DELAY_MS = 1_000L
46
+ private const val TOKEN_MAX_RETRY_DELAY_MS = 8_000L
47
+
48
+ private const val PROVIDER_FCM = "fcm"
49
+ private const val PROVIDER_HMS = "hms"
50
+ private const val PROVIDER_MI_PUSH = "mi_push"
51
+ private const val PROVIDER_OPPO_PUSH = "oppo_push"
52
+ private const val PROVIDER_VIVO_PUSH = "vivo_push"
53
+ private const val PROVIDER_MEIZU_PUSH = "meizu_push"
54
+ private const val PROVIDER_UNKNOWN = "unknown"
55
+
56
+ /** Service apps used to positively confirm an alternative provider on the device. */
57
+ private const val PACKAGE_HMS = "com.huawei.hwid"
58
+ private const val PACKAGE_MI_PUSH = "com.xiaomi.xmsf"
59
+
60
+ private const val TAG = "PushSignal"
29
61
 
30
62
  private val lock = Any()
31
63
  private val mainHandler = Handler(Looper.getMainLooper())
64
+ private val notifyExecutor = Executors.newSingleThreadExecutor()
32
65
  @Volatile private var application: Application? = null
33
66
  @Volatile private var currentActivity: Activity? = null
34
67
  @Volatile private var onMessage: ((PushMessage) -> Unit)? = null
@@ -98,7 +131,9 @@ internal object PushSignalCenter : Application.ActivityLifecycleCallbacks {
98
131
 
99
132
  fun fetchToken(): String {
100
133
  val context = application
101
- ?: throw IllegalStateException("PushSignal is not initialized")
134
+ ?: throw PushSignalException("E_NOT_INITIALIZED", "PushSignal is not initialized")
135
+
136
+ ensurePlayServices(context)
102
137
 
103
138
  try {
104
139
  if (FirebaseApp.getApps(context).isEmpty()) {
@@ -106,29 +141,229 @@ internal object PushSignalCenter : Application.ActivityLifecycleCallbacks {
106
141
  }
107
142
  FirebaseApp.getInstance()
108
143
  } catch (error: IllegalStateException) {
109
- throw IllegalStateException(
144
+ throw PushSignalException(
145
+ "E_FIREBASE_CONFIG",
110
146
  "Firebase is not configured. Call initialize({ project_id, mobilesdk_app_id, current_key, project_number }) or add google-services.json.",
111
147
  error
112
148
  )
113
149
  }
114
150
 
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
- }
151
+ val token = awaitTokenWithRetry(context)
124
152
 
125
153
  if (token.isNullOrEmpty()) {
126
- throw IllegalStateException("Firebase returned an empty FCM token")
154
+ throw PushSignalException("E_FCM_TOKEN", "Firebase returned an empty FCM token")
127
155
  }
128
156
 
129
157
  return token
130
158
  }
131
159
 
160
+ /**
161
+ * Fails fast with an actionable message when Google Play services cannot serve FCM
162
+ * (for example a China-only ROM without GMS). The message names the provider the
163
+ * device should use instead, so the cause is obvious in JS logs.
164
+ */
165
+ private fun ensurePlayServices(context: Context) {
166
+ val status = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context)
167
+ if (status == ConnectionResult.SUCCESS) {
168
+ return
169
+ }
170
+
171
+ val code = when (status) {
172
+ ConnectionResult.SERVICE_MISSING -> "E_GMS_MISSING"
173
+ ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED -> "E_GMS_UPDATE_REQUIRED"
174
+ ConnectionResult.SERVICE_DISABLED -> "E_GMS_DISABLED"
175
+ ConnectionResult.SERVICE_INVALID -> "E_GMS_INVALID"
176
+ else -> "E_GMS_UNAVAILABLE"
177
+ }
178
+
179
+ val reason = when (status) {
180
+ ConnectionResult.SERVICE_MISSING ->
181
+ "Google Play services are missing on this device, so the standard Google push service (FCM) cannot be used."
182
+ ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED ->
183
+ "Google Play services are outdated. Update them in the Play Store and try again."
184
+ ConnectionResult.SERVICE_DISABLED ->
185
+ "Google Play services are disabled. Enable them in the device settings and try again."
186
+ ConnectionResult.SERVICE_INVALID ->
187
+ "Google Play services are invalid or corrupted on this device. Reinstalling them usually helps."
188
+ else ->
189
+ "Google Play services are unavailable (code $status), so the standard Google push service (FCM) cannot be used."
190
+ }
191
+
192
+ val diagnostics = diagnose(context)
193
+ val message = listOfNotNull(reason, diagnostics.hint).joinToString(" ")
194
+ Log.w(TAG, "Play services check failed ($code): $message")
195
+ throw PushSignalException(code, message)
196
+ }
197
+
198
+ /**
199
+ * Collects device and provider diagnostics without throwing. Used both by the
200
+ * `getDiagnostics` API and to enrich errors with a concrete replacement service.
201
+ */
202
+ fun diagnose(context: Context): PushDiagnostics {
203
+ val status = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context)
204
+ val gmsAvailable = status == ConnectionResult.SUCCESS
205
+ val provider = resolveProvider(context, gmsAvailable)
206
+ val hint = providerHint(provider, gmsAvailable)
207
+
208
+ return PushDiagnostics(
209
+ platform = "android",
210
+ gmsAvailable = gmsAvailable,
211
+ gmsStatus = status,
212
+ manufacturer = Build.MANUFACTURER ?: "",
213
+ brand = Build.BRAND ?: "",
214
+ model = Build.MODEL ?: "",
215
+ provider = provider.id,
216
+ providerName = provider.name,
217
+ providerInstalled = provider.installed,
218
+ hint = hint,
219
+ )
220
+ }
221
+
222
+ private data class ProviderInfo(
223
+ val id: String,
224
+ val name: String,
225
+ val installed: Boolean?,
226
+ )
227
+
228
+ /**
229
+ * Picks the push provider this device should use. Checks the standard Google
230
+ * service first, then positively confirms an installed alternative, then falls
231
+ * back to the manufacturer mapping. Class reflection cannot see another
232
+ * provider's SDK unless the host bundles it, so package checks and the
233
+ * manufacturer are the reliable signals.
234
+ */
235
+ private fun resolveProvider(context: Context, gmsAvailable: Boolean): ProviderInfo {
236
+ if (gmsAvailable) {
237
+ return ProviderInfo(PROVIDER_FCM, "Firebase Cloud Messaging (FCM)", true)
238
+ }
239
+
240
+ if (isPackageInstalled(context, PACKAGE_HMS)) {
241
+ return ProviderInfo(PROVIDER_HMS, "HMS Push Kit (Huawei Push)", true)
242
+ }
243
+ if (isPackageInstalled(context, PACKAGE_MI_PUSH)) {
244
+ return ProviderInfo(PROVIDER_MI_PUSH, "Mi Push (Xiaomi Push)", true)
245
+ }
246
+
247
+ val keys = setOf(Build.MANUFACTURER, Build.BRAND)
248
+ .filterNotNull()
249
+ .map { it.uppercase() }
250
+
251
+ return when {
252
+ keys.any { it.contains("HUAWEI") || it.contains("HONOR") } ->
253
+ ProviderInfo(PROVIDER_HMS, "HMS Push Kit (Huawei Push)", false)
254
+ keys.any { it.contains("XIAOMI") || it.contains("REDMI") || it.contains("POCO") } ->
255
+ ProviderInfo(PROVIDER_MI_PUSH, "Mi Push (Xiaomi Push)", false)
256
+ keys.any { it.contains("OPPO") || it.contains("ONEPLUS") || it.contains("REALME") } ->
257
+ ProviderInfo(PROVIDER_OPPO_PUSH, "OPPO Push (HeyTap)", false)
258
+ keys.any { it.contains("VIVO") || it.contains("IQOO") } ->
259
+ ProviderInfo(PROVIDER_VIVO_PUSH, "vivo Push", false)
260
+ keys.any { it.contains("MEIZU") } ->
261
+ ProviderInfo(PROVIDER_MEIZU_PUSH, "Meizu Push", false)
262
+ else ->
263
+ ProviderInfo(PROVIDER_UNKNOWN, "unknown", null)
264
+ }
265
+ }
266
+
267
+ private fun providerHint(provider: ProviderInfo, gmsAvailable: Boolean): String? {
268
+ if (gmsAvailable) {
269
+ return null
270
+ }
271
+ val device = listOfNotNull(Build.MANUFACTURER, Build.BRAND)
272
+ .filter { it.isNotBlank() }
273
+ .distinct()
274
+ .joinToString("/")
275
+
276
+ return when (provider.id) {
277
+ PROVIDER_HMS ->
278
+ "This device ($device) has no usable Google Play services, so FCM will not issue a token. Use HMS Push Kit (Huawei Push) instead of the standard Google service."
279
+ PROVIDER_MI_PUSH ->
280
+ "This device ($device) has no usable Google Play services, so FCM will not issue a token. Use Mi Push (Xiaomi Push) instead of the standard Google service."
281
+ PROVIDER_OPPO_PUSH ->
282
+ "This device ($device) has no usable Google Play services, so FCM will not issue a token. Use OPPO Push (HeyTap) instead of the standard Google service."
283
+ PROVIDER_VIVO_PUSH ->
284
+ "This device ($device) has no usable Google Play services, so FCM will not issue a token. Use vivo Push instead of the standard Google service."
285
+ PROVIDER_MEIZU_PUSH ->
286
+ "This device ($device) has no usable Google Play services, so FCM will not issue a token. Use Meizu Push instead of the standard Google service."
287
+ else ->
288
+ "No usable Google Play services were found and no known push provider was detected on this ROM, so FCM cannot be used. Integrate another push provider (HMS, Mi Push, OPPO/vivo/Meizu Push)."
289
+ }
290
+ }
291
+
292
+ private fun isPackageInstalled(context: Context, packageName: String): Boolean {
293
+ return try {
294
+ context.packageManager.getPackageInfo(packageName, 0)
295
+ true
296
+ } catch (_: PackageManager.NameNotFoundException) {
297
+ false
298
+ } catch (_: Exception) {
299
+ false
300
+ }
301
+ }
302
+
303
+ /**
304
+ * FCM returns SERVICE_NOT_AVAILABLE for transient conditions (no Google account yet,
305
+ * Play services still starting, flaky network). Xiaomi/MIUI devices hit this often,
306
+ * so retry with backoff before surfacing the failure.
307
+ */
308
+ private fun awaitTokenWithRetry(context: Context): String? {
309
+ var delayMs = TOKEN_INITIAL_RETRY_DELAY_MS
310
+ var lastError: Exception? = null
311
+
312
+ for (attempt in 1..TOKEN_MAX_ATTEMPTS) {
313
+ try {
314
+ return Tasks.await(
315
+ FirebaseMessaging.getInstance().token,
316
+ TOKEN_TIMEOUT_SECONDS,
317
+ TimeUnit.SECONDS
318
+ )
319
+ } catch (error: Exception) {
320
+ lastError = error
321
+ if (attempt == TOKEN_MAX_ATTEMPTS || !isRetryableTokenError(error)) {
322
+ break
323
+ }
324
+ Log.w(TAG, "FCM token attempt $attempt/$TOKEN_MAX_ATTEMPTS failed: ${error.message}. Retrying in ${delayMs}ms")
325
+ try {
326
+ Thread.sleep(delayMs)
327
+ } catch (_: InterruptedException) {
328
+ Thread.currentThread().interrupt()
329
+ break
330
+ }
331
+ delayMs = (delayMs * 2).coerceAtMost(TOKEN_MAX_RETRY_DELAY_MS)
332
+ }
333
+ }
334
+
335
+ val cause = lastError
336
+ val diagnostics = diagnose(context)
337
+ val message = listOfNotNull(
338
+ "Failed to get an FCM token: ${cause?.message ?: "unknown error"}.",
339
+ "Make sure the device is signed into a Google account, Google Play services are up to date, " +
340
+ "and the app is allowed to use background data.",
341
+ "Call initialize({ project_id, mobilesdk_app_id, current_key, project_number }) or add google-services.json.",
342
+ diagnostics.hint,
343
+ ).joinToString(" ")
344
+ Log.w(TAG, "Failed to get an FCM token: $message")
345
+ throw PushSignalException("E_FCM_TOKEN", message, cause)
346
+ }
347
+
348
+ private fun isRetryableTokenError(error: Throwable): Boolean {
349
+ var current: Throwable? = error
350
+ while (current != null) {
351
+ if (current is java.io.IOException || current is java.util.concurrent.TimeoutException) {
352
+ return true
353
+ }
354
+ val message = current.message?.uppercase() ?: ""
355
+ if (
356
+ message.contains("SERVICE_NOT_AVAILABLE") ||
357
+ message.contains("TIMEOUT") ||
358
+ message.contains("INTERNAL_SERVER_ERROR")
359
+ ) {
360
+ return true
361
+ }
362
+ current = current.cause
363
+ }
364
+ return false
365
+ }
366
+
132
367
  fun emitMessage(remoteMessage: RemoteMessage) {
133
368
  val message = remoteMessage.toPushMessage()
134
369
  runOnMain { deliverMessage(message) }
@@ -169,18 +404,7 @@ internal object PushSignalCenter : Application.ActivityLifecycleCallbacks {
169
404
  val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
170
405
  ?: return
171
406
 
172
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
173
- val existing = manager.getNotificationChannel(CHANNEL_ID)
174
- if (existing == null) {
175
- manager.createNotificationChannel(
176
- NotificationChannel(
177
- CHANNEL_ID,
178
- "Notifications",
179
- NotificationManager.IMPORTANCE_HIGH
180
- )
181
- )
182
- }
183
- }
407
+ ensureChannel(manager)
184
408
 
185
409
  val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
186
410
  ?: return
@@ -188,6 +412,7 @@ internal object PushSignalCenter : Application.ActivityLifecycleCallbacks {
188
412
  launchIntent.putExtra("google.message_id", message.id ?: UUID.randomUUID().toString())
189
413
  message.title?.let { launchIntent.putExtra("gcm.notification.title", it) }
190
414
  message.body?.let { launchIntent.putExtra("gcm.notification.body", it) }
415
+ message.imageUrl?.let { launchIntent.putExtra("imageUrl", it) }
191
416
  message.data.forEach { (key, value) ->
192
417
  launchIntent.putExtra(key, value)
193
418
  }
@@ -200,6 +425,42 @@ internal object PushSignalCenter : Application.ActivityLifecycleCallbacks {
200
425
  PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
201
426
  )
202
427
 
428
+ val imageUrl = message.imageUrl
429
+ if (imageUrl.isNullOrBlank()) {
430
+ manager.notify(requestCode, buildNotification(context, message, pendingIntent, null))
431
+ return
432
+ }
433
+
434
+ notifyExecutor.execute {
435
+ val bitmap = downloadBitmap(imageUrl)
436
+ runOnMain {
437
+ manager.notify(requestCode, buildNotification(context, message, pendingIntent, bitmap))
438
+ }
439
+ }
440
+ }
441
+
442
+ private fun ensureChannel(manager: NotificationManager) {
443
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
444
+ return
445
+ }
446
+ if (manager.getNotificationChannel(CHANNEL_ID) != null) {
447
+ return
448
+ }
449
+ manager.createNotificationChannel(
450
+ NotificationChannel(
451
+ CHANNEL_ID,
452
+ "Notifications",
453
+ NotificationManager.IMPORTANCE_HIGH
454
+ )
455
+ )
456
+ }
457
+
458
+ private fun buildNotification(
459
+ context: Context,
460
+ message: PushMessage,
461
+ pendingIntent: PendingIntent,
462
+ bitmap: Bitmap?,
463
+ ): android.app.Notification {
203
464
  val builder = NotificationCompat.Builder(context, CHANNEL_ID)
204
465
  .setSmallIcon(smallIcon(context))
205
466
  .setContentTitle(message.title.orEmpty())
@@ -210,7 +471,48 @@ internal object PushSignalCenter : Application.ActivityLifecycleCallbacks {
210
471
  .setDefaults(NotificationCompat.DEFAULT_SOUND)
211
472
  .setNumber(1)
212
473
 
213
- manager.notify(requestCode, builder.build())
474
+ if (bitmap != null) {
475
+ builder
476
+ .setLargeIcon(bitmap)
477
+ .setStyle(
478
+ NotificationCompat.BigPictureStyle()
479
+ .bigPicture(bitmap)
480
+ .bigLargeIcon(null as Bitmap?)
481
+ )
482
+ }
483
+
484
+ return builder.build()
485
+ }
486
+
487
+ private fun downloadBitmap(imageUrl: String): Bitmap? {
488
+ var connection: HttpURLConnection? = null
489
+ return try {
490
+ connection = (URL(imageUrl).openConnection() as HttpURLConnection).apply {
491
+ connectTimeout = IMAGE_CONNECT_TIMEOUT_MS
492
+ readTimeout = IMAGE_READ_TIMEOUT_MS
493
+ instanceFollowRedirects = true
494
+ requestMethod = "GET"
495
+ }
496
+ val code = connection.responseCode
497
+ if (code !in 200..299) {
498
+ return null
499
+ }
500
+ val length = connection.contentLengthLong
501
+ if (length > IMAGE_MAX_BYTES) {
502
+ return null
503
+ }
504
+ connection.inputStream.use { input ->
505
+ val bytes = input.readBytes()
506
+ if (bytes.size > IMAGE_MAX_BYTES) {
507
+ return null
508
+ }
509
+ BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
510
+ }
511
+ } catch (_: Exception) {
512
+ null
513
+ } finally {
514
+ connection?.disconnect()
515
+ }
214
516
  }
215
517
 
216
518
  private fun smallIcon(context: Context): Int {
@@ -374,7 +676,8 @@ private fun Intent.toPushMessage(): PushMessage {
374
676
  extras.getString("gcm.n.body")
375
677
  ?: extras.getString("gcm.notification.body")
376
678
  ?: extras.getString("body"),
377
- data
679
+ data,
680
+ resolveImageUrl(null, data),
378
681
  )
379
682
  }
380
683
 
@@ -387,11 +690,18 @@ private fun AndroidFirebaseConfig.hasRequiredFields(): Boolean {
387
690
 
388
691
  private const val EXTRA_HANDLED_KEY = "pushsignal.handled"
389
692
 
693
+ internal fun resolveImageUrl(notificationImageUrl: String?, data: Map<String, String>): String? {
694
+ return notificationImageUrl?.takeIf { it.isNotBlank() }
695
+ ?: data["image"]?.takeIf { it.isNotBlank() }
696
+ ?: data["imageUrl"]?.takeIf { it.isNotBlank() }
697
+ }
698
+
390
699
  internal fun RemoteMessage.toPushMessage(): PushMessage {
391
700
  return PushMessage(
392
701
  messageId,
393
702
  notification?.title,
394
703
  notification?.body,
395
- data
704
+ data,
705
+ resolveImageUrl(notification?.imageUrl?.toString(), data),
396
706
  )
397
707
  }
@@ -50,7 +50,8 @@ class PushSignalPlugin : Plugin() {
50
50
  if (error == null) {
51
51
  call.resolve()
52
52
  } else {
53
- call.reject(error.message, error)
53
+ val code = (error as? PushSignalException)?.code ?: "E_INIT"
54
+ call.reject(error.message, code, error, diagnosticsData())
54
55
  }
55
56
  }
56
57
  }
@@ -63,18 +64,38 @@ class PushSignalPlugin : Plugin() {
63
64
  val token = PushSignalCenter.fetchToken()
64
65
  val result =
65
66
  JSObject().apply {
66
- put("platform", "android")
67
67
  put("token", token)
68
68
  }
69
69
  call.resolve(result)
70
70
  } catch (error: Exception) {
71
- call.reject(error.message, error)
71
+ val code = (error as? PushSignalException)?.code ?: "E_CREDENTIALS"
72
+ call.reject(error.message, code, error, diagnosticsData())
72
73
  }
73
74
  }
74
75
  }
75
76
 
76
77
  @PluginMethod
77
- fun checkPermissions(call: PluginCall) {
78
+ fun getDiagnostics(call: PluginCall) {
79
+ executor.execute {
80
+ try {
81
+ PushSignalCenter.attach(context)
82
+ call.resolve(PushSignalCenter.diagnose(context).toJSObject())
83
+ } catch (error: Exception) {
84
+ call.reject(error.message, "E_DIAGNOSTICS", error)
85
+ }
86
+ }
87
+ }
88
+
89
+ private fun diagnosticsData(): JSObject? {
90
+ return try {
91
+ PushSignalCenter.diagnose(context).toJSObject()
92
+ } catch (_: Exception) {
93
+ null
94
+ }
95
+ }
96
+
97
+ @PluginMethod
98
+ override fun checkPermissions(call: PluginCall) {
78
99
  if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
79
100
  val result = JSObject()
80
101
  result.put("receive", "granted")
@@ -85,7 +106,7 @@ class PushSignalPlugin : Plugin() {
85
106
  }
86
107
 
87
108
  @PluginMethod
88
- fun requestPermissions(call: PluginCall) {
109
+ override fun requestPermissions(call: PluginCall) {
89
110
  if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU ||
90
111
  getPermissionState(PUSH_NOTIFICATIONS) == PermissionState.GRANTED
91
112
  ) {
@@ -122,6 +143,7 @@ private fun PushMessage.toJSObject(): JSObject {
122
143
  id?.let { map.put("id", it) }
123
144
  title?.let { map.put("title", it) }
124
145
  body?.let { map.put("body", it) }
146
+ imageUrl?.let { map.put("imageUrl", it) }
125
147
  val dataMap = JSObject()
126
148
  data.forEach { (key, value) ->
127
149
  dataMap.put(key, value)
@@ -129,3 +151,18 @@ private fun PushMessage.toJSObject(): JSObject {
129
151
  map.put("data", dataMap)
130
152
  return map
131
153
  }
154
+
155
+ private fun PushDiagnostics.toJSObject(): JSObject {
156
+ val map = JSObject()
157
+ map.put("platform", platform)
158
+ map.put("gmsAvailable", gmsAvailable)
159
+ map.put("gmsStatus", gmsStatus)
160
+ map.put("manufacturer", manufacturer)
161
+ map.put("brand", brand)
162
+ map.put("model", model)
163
+ map.put("provider", provider)
164
+ map.put("providerName", providerName)
165
+ providerInstalled?.let { map.put("providerInstalled", it) }
166
+ hint?.let { map.put("hint", it) }
167
+ return map
168
+ }