@pushmesh/sdk 0.7.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/LICENSE +21 -0
- package/README.md +160 -0
- package/android/build.gradle +82 -0
- package/android/src/main/AndroidManifest.xml +69 -0
- package/android/src/main/java/io/pushmesh/sdk/PushMeshClickActivity.kt +124 -0
- package/android/src/main/java/io/pushmesh/sdk/PushMeshFirebaseModule.kt +512 -0
- package/android/src/main/java/io/pushmesh/sdk/PushMeshFirebasePackage.kt +20 -0
- package/android/src/main/java/io/pushmesh/sdk/PushMeshMessageBuffer.kt +53 -0
- package/android/src/main/java/io/pushmesh/sdk/PushMeshMessageRouter.kt +477 -0
- package/android/src/main/java/io/pushmesh/sdk/PushMeshMessagingReceiver.kt +77 -0
- package/android/src/main/java/io/pushmesh/sdk/PushMeshMessagingService.kt +88 -0
- package/dist/cjs/boot.d.ts +7 -0
- package/dist/cjs/boot.js +1 -0
- package/dist/cjs/compat.d.ts +63 -0
- package/dist/cjs/compat.js +1 -0
- package/dist/cjs/doctor.d.ts +27 -0
- package/dist/cjs/doctor.js +2 -0
- package/dist/cjs/events.d.ts +21 -0
- package/dist/cjs/events.js +1 -0
- package/dist/cjs/http.d.ts +20 -0
- package/dist/cjs/http.js +1 -0
- package/dist/cjs/inapp/SkeletonShimmer.d.ts +1 -0
- package/dist/cjs/inapp/SkeletonShimmer.js +1 -0
- package/dist/cjs/inapp/auto.d.ts +1 -0
- package/dist/cjs/inapp/auto.js +1 -0
- package/dist/cjs/inapp/components.d.ts +14 -0
- package/dist/cjs/inapp/components.js +1 -0
- package/dist/cjs/inapp/engine.d.ts +1 -0
- package/dist/cjs/inapp/engine.js +1 -0
- package/dist/cjs/inapp/imageCache.d.ts +1 -0
- package/dist/cjs/inapp/imageCache.js +1 -0
- package/dist/cjs/inapp/index.d.ts +20 -0
- package/dist/cjs/inapp/index.js +1 -0
- package/dist/cjs/inapp/lifecycle.d.ts +1 -0
- package/dist/cjs/inapp/lifecycle.js +1 -0
- package/dist/cjs/inapp/normalize.d.ts +1 -0
- package/dist/cjs/inapp/normalize.js +1 -0
- package/dist/cjs/inapp/registro.d.ts +1 -0
- package/dist/cjs/inapp/registro.js +1 -0
- package/dist/cjs/inapp/sessao.d.ts +1 -0
- package/dist/cjs/inapp/sessao.js +1 -0
- package/dist/cjs/index.d.ts +118 -0
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/perfil.d.ts +52 -0
- package/dist/cjs/perfil.js +1 -0
- package/dist/cjs/permissions.d.ts +54 -0
- package/dist/cjs/permissions.js +1 -0
- package/dist/cjs/players.d.ts +20 -0
- package/dist/cjs/players.js +1 -0
- package/dist/cjs/queue.d.ts +1 -0
- package/dist/cjs/queue.js +1 -0
- package/dist/cjs/receipts.d.ts +32 -0
- package/dist/cjs/receipts.js +1 -0
- package/dist/cjs/state.d.ts +21 -0
- package/dist/cjs/state.js +1 -0
- package/dist/cjs/storage.d.ts +1 -0
- package/dist/cjs/storage.js +1 -0
- package/dist/cjs/tags.d.ts +13 -0
- package/dist/cjs/tags.js +1 -0
- package/dist/cjs/triggers.d.ts +22 -0
- package/dist/cjs/triggers.js +1 -0
- package/dist/cjs/types.d.ts +258 -0
- package/dist/cjs/types.js +1 -0
- package/dist/cjs/zeroconfig.d.ts +45 -0
- package/dist/cjs/zeroconfig.js +1 -0
- package/ios/NotificationService/LEIA-ME.md +105 -0
- package/ios/NotificationService/PushMeshNotificationService.swift +384 -0
- package/ios/PushMeshApns.swift +746 -0
- package/ios/PushMeshApnsModule.m +76 -0
- package/ios/PushMeshSwizzleRegistro.swift +141 -0
- package/package.json +113 -0
- package/pushmesh-sdk.podspec +32 -0
- package/react-native.config.js +17 -0
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
package io.pushmesh.sdk
|
|
2
|
+
|
|
3
|
+
import android.app.Notification
|
|
4
|
+
import android.app.NotificationChannel
|
|
5
|
+
import android.app.NotificationManager
|
|
6
|
+
import android.app.PendingIntent
|
|
7
|
+
import android.content.Context
|
|
8
|
+
import android.content.Intent
|
|
9
|
+
import android.graphics.Bitmap
|
|
10
|
+
import android.graphics.BitmapFactory
|
|
11
|
+
import android.graphics.Color
|
|
12
|
+
import android.media.RingtoneManager
|
|
13
|
+
import android.net.Uri
|
|
14
|
+
import android.os.Build
|
|
15
|
+
import android.os.SystemClock
|
|
16
|
+
import androidx.core.app.NotificationCompat
|
|
17
|
+
import com.facebook.react.ReactApplication
|
|
18
|
+
import com.facebook.react.bridge.Arguments
|
|
19
|
+
import com.facebook.react.bridge.ReactContext
|
|
20
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
21
|
+
import org.json.JSONObject
|
|
22
|
+
import java.io.ByteArrayOutputStream
|
|
23
|
+
import java.io.InputStream
|
|
24
|
+
import java.net.HttpURLConnection
|
|
25
|
+
import java.net.URL
|
|
26
|
+
import kotlin.concurrent.thread
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Roteador central das mensagens PUSHMESH no Android (modo zero-config).
|
|
30
|
+
* Usado pelo [PushMeshMessagingReceiver] (dual-run — caminho principal) e
|
|
31
|
+
* pelo [PushMeshMessagingService] (fallback, quando a cadeia de broadcast
|
|
32
|
+
* não passa pelo receiver).
|
|
33
|
+
*
|
|
34
|
+
* Com o corpo DATA-ONLY do servidor (decisão 2026-07-28), NADA é exibido pelo
|
|
35
|
+
* sistema — então em TODOS os estados (foreground/background/morto) este
|
|
36
|
+
* router:
|
|
37
|
+
* 1. avisa o JS (PushMeshMessage) se há instância ativa, ou bufferiza para
|
|
38
|
+
* o próximo init (PushMeshMessageBuffer);
|
|
39
|
+
* 2. exibe a notificação via NotificationCompat (título/corpo/imagem/canal/
|
|
40
|
+
* som/cor do `pm_*`; imagem baixada com timeout, TETO DE MEMÓRIA e
|
|
41
|
+
* fallback sem imagem);
|
|
42
|
+
* 3. posta o RECIBO DE ENTREGA direto em Kotlin (não depende de JS vivo nem
|
|
43
|
+
* de o usuário abrir o app; o JS reconcilia/deduplica pelo pm_msg_id e o
|
|
44
|
+
* servidor deduplica pelo pm:rcpt NX — um eventual recibo duplo via JS é
|
|
45
|
+
* absorvido lá).
|
|
46
|
+
*
|
|
47
|
+
* Tudo dentro de uma JANELA de tempo: ver o KDoc de [handle].
|
|
48
|
+
*/
|
|
49
|
+
object PushMeshMessageRouter {
|
|
50
|
+
|
|
51
|
+
const val CANAL_PADRAO = "pushmesh_default"
|
|
52
|
+
const val PREFS = "pushmesh_prefs"
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Modo de inicialização do FCM escolhido pelo `configure()`
|
|
56
|
+
* (PushMeshFirebaseModule) e persistido aqui porque quem precisa dele —
|
|
57
|
+
* o [PushMeshMessagingService.onNewToken] — roda com o JS morto, em
|
|
58
|
+
* processo recém-acordado pelo sistema, onde o módulo nativo nem existe.
|
|
59
|
+
*/
|
|
60
|
+
const val CHAVE_MODO_FCM = "pmx_fcm_mode"
|
|
61
|
+
|
|
62
|
+
/** Nós somos o FirebaseApp DEFAULT: o token do onNewToken é NOSSO. */
|
|
63
|
+
const val MODO_DEFAULT = "default"
|
|
64
|
+
|
|
65
|
+
/** O host é o DEFAULT e nós somos o app secundário: onNewToken NÃO é nosso. */
|
|
66
|
+
const val MODO_SECUNDARIO = "secundario"
|
|
67
|
+
|
|
68
|
+
private const val TAG = "PushMeshRouter"
|
|
69
|
+
private const val TIMEOUT_IMG_MS = 5_000
|
|
70
|
+
private const val TIMEOUT_HTTP_MS = 8_000
|
|
71
|
+
private const val PAUSA_RETRY_MS = 1_000L
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Janela total do trabalho de UMA mensagem. O receiver segura o broadcast
|
|
75
|
+
* com goAsync() e o service do FCM segura um wakelock — os dois têm limite
|
|
76
|
+
* de ~10s, e estourá-lo é ANR. 9s deixa folga para o finish().
|
|
77
|
+
*/
|
|
78
|
+
private const val JANELA_TOTAL_MS = 9_000L
|
|
79
|
+
|
|
80
|
+
/** Teto do corpo da imagem baixada (ver [baixarImagem]). */
|
|
81
|
+
private const val MAX_BYTES_IMG = 4L * 1024 * 1024
|
|
82
|
+
|
|
83
|
+
/** Teto de pixels do bitmap decodificado — 2 MP (ver [baixarImagem]). */
|
|
84
|
+
private const val MAX_PIXELS_IMG = 2_000_000L
|
|
85
|
+
|
|
86
|
+
fun prefs(context: Context) =
|
|
87
|
+
context.applicationContext.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Marcador do dual-run: a mensagem é NOSSA se tem alguma chave `pm_*`
|
|
91
|
+
* (o servidor manda o bloco de exibição nessas chaves). ESPELHO da função
|
|
92
|
+
* JS `mensagemDoPushMesh` (src/zeroconfig.ts) — mudou aqui, muda lá.
|
|
93
|
+
* A prova real do roteamento no manifest mesclado é em device (o push do
|
|
94
|
+
* provedor legado, sem pm_*, passa intocado).
|
|
95
|
+
*/
|
|
96
|
+
fun isNossa(data: Map<String, String>): Boolean = data.keys.any { it.startsWith("pmx_") }
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Contexto React vivo — nas DUAS arquiteturas e SEM NUNCA lançar.
|
|
100
|
+
*
|
|
101
|
+
* Armadilha que derrubou o app piloto em produção (2026-07-29): na New
|
|
102
|
+
* Architecture o getter `ReactApplication.reactNativeHost` **lança
|
|
103
|
+
* RuntimeException de propósito** ("You should not use ReactNativeHost
|
|
104
|
+
* directly in the New Architecture"). Como o receiver/service rodam em
|
|
105
|
+
* thread de broadcast do sistema, a exceção não é "erro de push": ela
|
|
106
|
+
* DERRUBA O APP INTEIRO a cada mensagem recebida.
|
|
107
|
+
*
|
|
108
|
+
* Ordem: `reactHost` primeiro (New Architecture, default do RN desde 0.76);
|
|
109
|
+
* `reactNativeHost` só como fallback para app em arquitetura antiga, dentro
|
|
110
|
+
* de try/catch. Contexto ausente NÃO é falha: o router bufferiza para o
|
|
111
|
+
* próximo init e a exibição/recibo nativos independem de JS.
|
|
112
|
+
*/
|
|
113
|
+
fun contextReact(context: Context): ReactContext? {
|
|
114
|
+
val app = context.applicationContext as? ReactApplication ?: return null
|
|
115
|
+
try {
|
|
116
|
+
app.reactHost?.currentReactContext?.let { return it }
|
|
117
|
+
} catch (_: Throwable) {
|
|
118
|
+
// host novo indisponível — cai para o caminho antigo
|
|
119
|
+
}
|
|
120
|
+
return try {
|
|
121
|
+
@Suppress("DEPRECATION")
|
|
122
|
+
app.reactNativeHost.reactInstanceManager.currentReactContext
|
|
123
|
+
} catch (_: Throwable) {
|
|
124
|
+
null
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Pipeline completo de uma mensagem nossa (ver KDoc da classe).
|
|
130
|
+
*
|
|
131
|
+
* BLOQUEANTE DE PROPÓSITO (2026-08-01) — só chame de thread de background
|
|
132
|
+
* (o `goAsync` do receiver ou a thread do FCM no service).
|
|
133
|
+
*
|
|
134
|
+
* POR QUÊ: quem chama só mantém o processo com prioridade enquanto o
|
|
135
|
+
* callback NÃO retorna. Na versão anterior o recibo e a exibição saíam em
|
|
136
|
+
* `thread {}` soltas e handle() voltava em milissegundos — o
|
|
137
|
+
* `pending.finish()` acontecia logo em seguida e, com o app MORTO (o caso
|
|
138
|
+
* que o receiver existe para cobrir), o processo virava "cached" na hora.
|
|
139
|
+
* Sob pressão de memória — justamente nos OEMs agressivos que o SDK mede em
|
|
140
|
+
* `bateria_irrestrita` — o LMK matava o processo NO MEIO do download da
|
|
141
|
+
* imagem: push que nunca apareceu e recibo perdido, sem erro em lugar
|
|
142
|
+
* nenhum. Agora o trabalho termina ANTES de handle() voltar.
|
|
143
|
+
*
|
|
144
|
+
* JANELA: a do broadcast/wakelock é de ~10s e estourá-la é ANR, então tudo
|
|
145
|
+
* respeita [JANELA_TOTAL_MS]. A ordem é deliberada:
|
|
146
|
+
* 1. evento/buffer do JS — instantâneo, sem rede, e é o FALLBACK durável do
|
|
147
|
+
* recibo (o JS drena no próximo init);
|
|
148
|
+
* 2. exibição — é o que o usuário vê e o que NÃO tem segunda chance;
|
|
149
|
+
* 3. recibo direto — com o que sobrar da janela; se não couber, o buffer +
|
|
150
|
+
* o próximo init reenviam (o servidor deduplica pelo pm:rcpt NX).
|
|
151
|
+
*
|
|
152
|
+
* Cada passo é isolado em seu próprio `runCatching`: falha de um não
|
|
153
|
+
* impede os outros e NADA escapa daqui (LEI do SDK: push nunca derruba o
|
|
154
|
+
* app do cliente).
|
|
155
|
+
*/
|
|
156
|
+
fun handle(context: Context, data: Map<String, String>, reactContext: ReactContext?) {
|
|
157
|
+
val prazo = SystemClock.elapsedRealtime() + JANELA_TOTAL_MS
|
|
158
|
+
runCatching { avisarJs(context, data, reactContext) }
|
|
159
|
+
.onFailure { android.util.Log.w(TAG, "aviso ao JS falhou (o resto do push segue)", it) }
|
|
160
|
+
runCatching { exibirAgora(context, data, prazo) }
|
|
161
|
+
.onFailure { android.util.Log.w(TAG, "exibição do push falhou (app segue normal)", it) }
|
|
162
|
+
runCatching { postarReciboAgora(context, data, "recebido", prazo) }
|
|
163
|
+
.onFailure { android.util.Log.w(TAG, "recibo direto falhou (o buffer reenvia)", it) }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Evento para o JS vivo; sem instância ativa, bufferiza para o próximo init. */
|
|
167
|
+
private fun avisarJs(context: Context, data: Map<String, String>, reactContext: ReactContext?) {
|
|
168
|
+
if (reactContext != null && reactContext.hasActiveReactInstance()) {
|
|
169
|
+
val dataMap = Arguments.createMap()
|
|
170
|
+
for ((k, v) in data) dataMap.putString(k, v)
|
|
171
|
+
val payload = Arguments.createMap()
|
|
172
|
+
payload.putMap("data", dataMap)
|
|
173
|
+
reactContext
|
|
174
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
175
|
+
.emit(PushMeshFirebaseModule.EVENT_MESSAGE, payload)
|
|
176
|
+
} else {
|
|
177
|
+
PushMeshMessageBuffer.add(context, data)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Token FCM novo chegou sem JS (ou com JS): sempre persiste — o boot reconcilia. */
|
|
182
|
+
fun salvarTokenPendente(context: Context, token: String) {
|
|
183
|
+
prefs(context).edit().putString("pmx_pending_token", token).apply()
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
fun tokenPendente(context: Context): String? = prefs(context).getString("pmx_pending_token", null)
|
|
187
|
+
|
|
188
|
+
fun limparTokenPendente(context: Context) {
|
|
189
|
+
prefs(context).edit().remove("pmx_pending_token").apply()
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Grava o modo do FCM decidido no configure() (ver [CHAVE_MODO_FCM]). */
|
|
193
|
+
fun salvarModoFcm(context: Context, modo: String) {
|
|
194
|
+
prefs(context).edit().putString(CHAVE_MODO_FCM, modo).apply()
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Modo do FCM persistido. O default é [MODO_DEFAULT] porque esse é o caminho
|
|
199
|
+
* principal do zero-config (app do cliente sem Firebase): antes do primeiro
|
|
200
|
+
* `configure()` o token que chegar é, quase sempre, do FirebaseApp que nós
|
|
201
|
+
* mesmos vamos assumir — e o boot ainda reconcilia com um getToken fresco.
|
|
202
|
+
*/
|
|
203
|
+
fun modoFcm(context: Context): String =
|
|
204
|
+
prefs(context).getString(CHAVE_MODO_FCM, MODO_DEFAULT) ?: MODO_DEFAULT
|
|
205
|
+
|
|
206
|
+
// -------------------------------------------------------------------------
|
|
207
|
+
// Recibo de entrega direto (1 retry leve, sempre dentro da janela)
|
|
208
|
+
// -------------------------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Recibo em thread própria (fire-and-forget) — para quem NÃO pode bloquear:
|
|
212
|
+
* o clique chega na main thread da [PushMeshClickActivity], onde rede
|
|
213
|
+
* síncrona seria NetworkOnMainThreadException.
|
|
214
|
+
*
|
|
215
|
+
* No caminho de ENTREGA use [postarReciboAgora] (dentro de handle): lá o
|
|
216
|
+
* trabalho precisa terminar antes de o processo perder prioridade.
|
|
217
|
+
*/
|
|
218
|
+
fun postarRecibo(context: Context, data: Map<String, String>, evento: String = "recebido") {
|
|
219
|
+
val prazo = SystemClock.elapsedRealtime() + JANELA_TOTAL_MS
|
|
220
|
+
thread {
|
|
221
|
+
// LEI do SDK: push nunca derruba o app do cliente. Exceção (ou Error)
|
|
222
|
+
// não tratada em thread própria mata o processo inteiro — aqui ela para.
|
|
223
|
+
try {
|
|
224
|
+
postarReciboAgora(context, data, evento, prazo)
|
|
225
|
+
} catch (t: Throwable) {
|
|
226
|
+
android.util.Log.w(TAG, "recibo em background falhou (app segue normal)", t)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Recibo SÍNCRONO: roda na thread de quem chamou e respeita o `prazo`
|
|
233
|
+
* (relógio de [SystemClock.elapsedRealtime]). Falhar aqui não perde o dado —
|
|
234
|
+
* o buffer nativo + o drain do JS no próximo init reenviam.
|
|
235
|
+
*/
|
|
236
|
+
private fun postarReciboAgora(
|
|
237
|
+
context: Context,
|
|
238
|
+
data: Map<String, String>,
|
|
239
|
+
evento: String,
|
|
240
|
+
prazo: Long,
|
|
241
|
+
) {
|
|
242
|
+
val msgId = data["pm_msg_id"] ?: return
|
|
243
|
+
val rcpt = data["pm_rcpt"] ?: return
|
|
244
|
+
val prefs = prefs(context)
|
|
245
|
+
// Faltando qualquer um: player ainda não registrado (1º boot). O recibo sai
|
|
246
|
+
// pelo JS quando o register acontecer e o buffer for drenado — nada se perde.
|
|
247
|
+
val baseUrl = prefs.getString("pmx_base_url", null)?.takeIf { it.isNotBlank() } ?: return
|
|
248
|
+
val appId = prefs.getString("pmx_app_id", null)?.takeIf { it.isNotBlank() } ?: return
|
|
249
|
+
val playerId = prefs.getString("pmx_player_id", null)?.takeIf { it.isNotBlank() } ?: return
|
|
250
|
+
val corpo = JSONObject()
|
|
251
|
+
.put("app_id", appId)
|
|
252
|
+
.put("notification_id", msgId)
|
|
253
|
+
.put("player_id", playerId)
|
|
254
|
+
.put("rcpt", rcpt)
|
|
255
|
+
.put("evento", evento)
|
|
256
|
+
.toString()
|
|
257
|
+
|
|
258
|
+
for (tentativa in 1..2) {
|
|
259
|
+
val restante = restanteMs(prazo)
|
|
260
|
+
if (restante <= 0L) break
|
|
261
|
+
val timeout = restante.coerceAtMost(TIMEOUT_HTTP_MS.toLong()).toInt()
|
|
262
|
+
if (enviarRecibo(baseUrl, corpo, timeout)) return
|
|
263
|
+
// Só pausa para a 2ª tentativa se ainda houver janela para a pausa MAIS
|
|
264
|
+
// um POST curto — senão a espera só serviria para estourar o limite.
|
|
265
|
+
if (tentativa == 1) {
|
|
266
|
+
if (restanteMs(prazo) < PAUSA_RETRY_MS + 1_000L) break
|
|
267
|
+
Thread.sleep(PAUSA_RETRY_MS)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
android.util.Log.w(TAG, "recibo direto não confirmou; o buffer/JS reenviam no próximo init")
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** POST do recibo; true = servidor confirmou (2xx). Nunca lança. */
|
|
274
|
+
private fun enviarRecibo(baseUrl: String, corpo: String, timeoutMs: Int): Boolean = try {
|
|
275
|
+
val conn = URL("$baseUrl/api/v1/receipts").openConnection() as HttpURLConnection
|
|
276
|
+
try {
|
|
277
|
+
conn.requestMethod = "POST"
|
|
278
|
+
conn.setRequestProperty("Content-Type", "application/json")
|
|
279
|
+
conn.connectTimeout = timeoutMs
|
|
280
|
+
conn.readTimeout = timeoutMs
|
|
281
|
+
conn.doOutput = true
|
|
282
|
+
conn.outputStream.use { it.write(corpo.toByteArray(Charsets.UTF_8)) }
|
|
283
|
+
conn.responseCode in 200..299
|
|
284
|
+
} finally {
|
|
285
|
+
conn.disconnect()
|
|
286
|
+
}
|
|
287
|
+
} catch (t: Throwable) {
|
|
288
|
+
android.util.Log.w(TAG, "recibo direto falhou: ${t.message}")
|
|
289
|
+
false
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Quanto sobrou da janela, nunca negativo. */
|
|
293
|
+
private fun restanteMs(prazo: Long): Long = (prazo - SystemClock.elapsedRealtime()).coerceAtLeast(0L)
|
|
294
|
+
|
|
295
|
+
// -------------------------------------------------------------------------
|
|
296
|
+
// Display via NotificationCompat (data-only: nós exibimos em todos os estados)
|
|
297
|
+
// -------------------------------------------------------------------------
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Exibe a notificação NA THREAD DE QUEM CHAMOU (ver KDoc de [handle]: em
|
|
301
|
+
* thread solta o processo podia morrer antes do `notify`). Respeita o prazo
|
|
302
|
+
* da janela no download da imagem.
|
|
303
|
+
*/
|
|
304
|
+
private fun exibirAgora(context: Context, data: Map<String, String>, prazo: Long) {
|
|
305
|
+
val title = data["pmx_title"]
|
|
306
|
+
val body = data["pmx_body"]
|
|
307
|
+
if (title.isNullOrBlank() && body.isNullOrBlank()) return
|
|
308
|
+
|
|
309
|
+
val bitmap = data["pmx_image"]?.takeIf { it.isNotBlank() }?.let { baixarImagem(it, restanteMs(prazo)) }
|
|
310
|
+
val channelId = data["pmx_channel"]?.takeIf { it.isNotBlank() } ?: CANAL_PADRAO
|
|
311
|
+
garantirCanal(context, channelId, data["pmx_sound"])
|
|
312
|
+
|
|
313
|
+
// CLIQUE: passa pelo NOSSO trampolim (PushMeshClickActivity).
|
|
314
|
+
//
|
|
315
|
+
// Por quê: quando a mensagem tem `pm_url`, o toque precisa abrir a URL —
|
|
316
|
+
// e se o PendingIntent fosse um ACTION_VIEW direto, o navegador abriria
|
|
317
|
+
// sem o nosso processo rodar e o CLIQUE NUNCA SERIA CONTADO (o funil
|
|
318
|
+
// ficaria zerado justamente nas campanhas com link, que são as que mais
|
|
319
|
+
// importam). O receiver registra o clique e só então abre o destino:
|
|
320
|
+
// a URL, quando existe; a MainActivity, quando não.
|
|
321
|
+
val notifId = data["pm_msg_id"]?.hashCode() ?: (System.currentTimeMillis() % 100_000).toInt()
|
|
322
|
+
val intentClique = Intent(context, PushMeshClickActivity::class.java).apply {
|
|
323
|
+
for ((k, v) in data) putExtra(k, v)
|
|
324
|
+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
325
|
+
}
|
|
326
|
+
// getActivity, NÃO getBroadcast: só um PendingIntent de Activity pode
|
|
327
|
+
// abrir tela a partir da notificação (Android 10+ bloqueia abertura de
|
|
328
|
+
// tela por app em segundo plano, e bloqueia em silêncio).
|
|
329
|
+
val pi = PendingIntent.getActivity(
|
|
330
|
+
context, notifId, intentClique,
|
|
331
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
val builder = NotificationCompat.Builder(context, channelId)
|
|
335
|
+
.setSmallIcon(resolverIcone(context, data["pmx_icon"]))
|
|
336
|
+
.setContentTitle(title)
|
|
337
|
+
.setContentText(body)
|
|
338
|
+
.setAutoCancel(true)
|
|
339
|
+
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
|
340
|
+
builder.setContentIntent(pi)
|
|
341
|
+
data["pmx_color"]?.let {
|
|
342
|
+
try {
|
|
343
|
+
builder.setColor(Color.parseColor(it))
|
|
344
|
+
} catch (_: IllegalArgumentException) {
|
|
345
|
+
// cor inválida: segue sem cor
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
resolverSom(context, data["pmx_sound"])?.let { builder.setSound(it) } // < API 26 (canal manda no 26+)
|
|
349
|
+
if (bitmap != null) {
|
|
350
|
+
builder.setStyle(NotificationCompat.BigPictureStyle().bigPicture(bitmap))
|
|
351
|
+
} else if (!body.isNullOrBlank()) {
|
|
352
|
+
builder.setStyle(NotificationCompat.BigTextStyle().bigText(body))
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
356
|
+
nm.notify(notifId, builder.build())
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private fun garantirCanal(context: Context, id: String, sound: String?) {
|
|
360
|
+
if (Build.VERSION.SDK_INT < 26) return
|
|
361
|
+
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
362
|
+
if (nm.getNotificationChannel(id) != null) return // canal do host: não mexe
|
|
363
|
+
val canal = NotificationChannel(
|
|
364
|
+
id,
|
|
365
|
+
if (id == CANAL_PADRAO) "Notificações" else id,
|
|
366
|
+
NotificationManager.IMPORTANCE_HIGH,
|
|
367
|
+
)
|
|
368
|
+
resolverSom(context, sound)?.let { canal.setSound(it, Notification.AUDIO_ATTRIBUTES_DEFAULT) }
|
|
369
|
+
nm.createNotificationChannel(canal)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private fun resolverSom(context: Context, sound: String?): Uri? {
|
|
373
|
+
if (sound.isNullOrBlank()) return null
|
|
374
|
+
if (sound == "default") return RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
|
|
375
|
+
val nome = sound.substringBeforeLast('.')
|
|
376
|
+
return Uri.parse("android.resource://${context.packageName}/raw/$nome")
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
private fun resolverIcone(context: Context, icon: String?): Int {
|
|
380
|
+
if (!icon.isNullOrBlank()) {
|
|
381
|
+
val porDrawable = context.resources.getIdentifier(icon, "drawable", context.packageName)
|
|
382
|
+
if (porDrawable != 0) return porDrawable
|
|
383
|
+
val porMipmap = context.resources.getIdentifier(icon, "mipmap", context.packageName)
|
|
384
|
+
if (porMipmap != 0) return porMipmap
|
|
385
|
+
}
|
|
386
|
+
return context.applicationInfo.icon
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Baixa a imagem do bigPicture com TETO DE MEMÓRIA e timeout; qualquer falha
|
|
391
|
+
* ⇒ null (a notificação sai SEM imagem — nunca sem notificação).
|
|
392
|
+
*
|
|
393
|
+
* ARMADILHA REAL (corrigida em 2026-08-01): decodificar direto do stream não
|
|
394
|
+
* tem teto nenhum. O bitmap custa largura × altura × 4 bytes, então uma
|
|
395
|
+
* campanha com foto de 12 MP (4000 × 3000) pede ~48 MB de uma vez — e o que
|
|
396
|
+
* estoura é `OutOfMemoryError`, que é `java.lang.Error`, NÃO `Exception`:
|
|
397
|
+
* um `catch (e: Exception)` deixa ele passar e, vindo de thread própria, ele
|
|
398
|
+
* MATA O PROCESSO do app do cliente. E não adianta confiar no upload do
|
|
399
|
+
* painel: a API compatível aceita a URL da imagem vinda de fora (big_picture),
|
|
400
|
+
* e um JPEG de 1 MB pode ser 8000 × 4000.
|
|
401
|
+
*
|
|
402
|
+
* Três defesas, nesta ordem:
|
|
403
|
+
* 1. recusar Content-Length absurdo ANTES de ler, e cortar a leitura em
|
|
404
|
+
* [MAX_BYTES_IMG] (servidor que mente no header não passa);
|
|
405
|
+
* 2. medir com `inJustDecodeBounds` e reamostrar com `inSampleSize` para no
|
|
406
|
+
* máximo [MAX_PIXELS_IMG] — o BigPictureStyle não usa mais que isso, então
|
|
407
|
+
* cortar pixel aqui não tira NADA da tela do usuário;
|
|
408
|
+
* 3. `catch (t: Throwable)` — é o único catch que pega OutOfMemoryError.
|
|
409
|
+
*/
|
|
410
|
+
private fun baixarImagem(url: String, tetoMs: Long): Bitmap? {
|
|
411
|
+
if (tetoMs <= 0L) return null // sem janela: melhor notificação sem imagem que push atrasado
|
|
412
|
+
return try {
|
|
413
|
+
val timeout = tetoMs.coerceAtMost(TIMEOUT_IMG_MS.toLong()).toInt()
|
|
414
|
+
val conn = URL(url).openConnection() as HttpURLConnection
|
|
415
|
+
try {
|
|
416
|
+
conn.connectTimeout = timeout
|
|
417
|
+
conn.readTimeout = timeout
|
|
418
|
+
val declarado = conn.contentLengthLong // -1 quando o servidor não declara
|
|
419
|
+
if (declarado > MAX_BYTES_IMG) {
|
|
420
|
+
android.util.Log.w(TAG, "imagem do push recusada: $declarado bytes declarados")
|
|
421
|
+
return null
|
|
422
|
+
}
|
|
423
|
+
val bytes = conn.inputStream.use { lerAteOTeto(it) }
|
|
424
|
+
?: run {
|
|
425
|
+
android.util.Log.w(TAG, "imagem do push recusada: corpo acima de $MAX_BYTES_IMG bytes")
|
|
426
|
+
return null
|
|
427
|
+
}
|
|
428
|
+
decodificarComTeto(bytes)
|
|
429
|
+
} finally {
|
|
430
|
+
conn.disconnect()
|
|
431
|
+
}
|
|
432
|
+
} catch (t: Throwable) {
|
|
433
|
+
// Throwable, não Exception: OutOfMemoryError é Error e é exatamente o
|
|
434
|
+
// que precisa morrer AQUI, e não no processo do cliente.
|
|
435
|
+
android.util.Log.w(TAG, "imagem do push falhou: ${t.message}")
|
|
436
|
+
null
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** Lê o corpo até [MAX_BYTES_IMG]; passou do teto ⇒ null (não guardamos lixo em RAM). */
|
|
441
|
+
private fun lerAteOTeto(entrada: InputStream): ByteArray? {
|
|
442
|
+
val saida = ByteArrayOutputStream()
|
|
443
|
+
val balde = ByteArray(16 * 1024)
|
|
444
|
+
var total = 0L
|
|
445
|
+
while (true) {
|
|
446
|
+
val lidos = entrada.read(balde)
|
|
447
|
+
if (lidos < 0) break
|
|
448
|
+
total += lidos
|
|
449
|
+
if (total > MAX_BYTES_IMG) return null
|
|
450
|
+
saida.write(balde, 0, lidos)
|
|
451
|
+
}
|
|
452
|
+
return saida.toByteArray()
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** Duas passadas: mede (sem alocar) e só então decodifica reamostrado. */
|
|
456
|
+
private fun decodificarComTeto(bytes: ByteArray): Bitmap? {
|
|
457
|
+
val medida = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
|
458
|
+
BitmapFactory.decodeByteArray(bytes, 0, bytes.size, medida)
|
|
459
|
+
val largura = medida.outWidth
|
|
460
|
+
val altura = medida.outHeight
|
|
461
|
+
if (largura <= 0 || altura <= 0) return null // não é imagem que sabemos ler
|
|
462
|
+
val opcoes = BitmapFactory.Options().apply { inSampleSize = amostragem(largura, altura) }
|
|
463
|
+
return BitmapFactory.decodeByteArray(bytes, 0, bytes.size, opcoes)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Menor potência de 2 que traz largura × altura para ≤ [MAX_PIXELS_IMG]
|
|
468
|
+
* (o `inSampleSize` só respeita potência de 2 — valor quebrado é arredondado
|
|
469
|
+
* para baixo pelo próprio BitmapFactory).
|
|
470
|
+
*/
|
|
471
|
+
private fun amostragem(largura: Int, altura: Int): Int {
|
|
472
|
+
val pixels = largura.toLong() * altura.toLong()
|
|
473
|
+
var amostra = 1
|
|
474
|
+
while (pixels / (amostra.toLong() * amostra.toLong()) > MAX_PIXELS_IMG) amostra *= 2
|
|
475
|
+
return amostra
|
|
476
|
+
}
|
|
477
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
package io.pushmesh.sdk
|
|
2
|
+
|
|
3
|
+
import android.content.BroadcastReceiver
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.Intent
|
|
6
|
+
import kotlin.concurrent.thread
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Receiver de ALTA PRIORIDADE para `com.google.android.c2dm.intent.RECEIVE` —
|
|
10
|
+
* a peça do DUAL-RUN (migração do provedor legado):
|
|
11
|
+
*
|
|
12
|
+
* No manifest mesclado, o FCM entrega a mensagem como broadcast ORDENADO —
|
|
13
|
+
* apenas UM service/receiver a processa. Sem este receiver, ou o push do
|
|
14
|
+
* provedor legado morre, ou o nosso nunca chega. A regra:
|
|
15
|
+
*
|
|
16
|
+
* - mensagem NOSSA (marcador `pm_*`, ver PushMeshMessageRouter.isNossa):
|
|
17
|
+
* processamos aqui (recibo direto + display NotificationCompat + evento/
|
|
18
|
+
* buffer JS) e `abortBroadcast()` — o SDK do provedor legado nem vê;
|
|
19
|
+
* - mensagem SEM o marcador: return imediato, SEM abort — o broadcast segue
|
|
20
|
+
* a cadeia e o SDK legado recebe normalmente (dual-run de verdade).
|
|
21
|
+
*
|
|
22
|
+
* A prova real do roteamento no manifest mesclado é em device (NÃO VERIFICADO
|
|
23
|
+
* em build); a decisão do marcador é testada em JS (espelho) e revisada aqui.
|
|
24
|
+
*/
|
|
25
|
+
class PushMeshMessagingReceiver : BroadcastReceiver() {
|
|
26
|
+
|
|
27
|
+
override fun onReceive(context: Context, intent: Intent) {
|
|
28
|
+
val extras = intent.extras ?: return
|
|
29
|
+
val data = HashMap<String, String>()
|
|
30
|
+
for (k in extras.keySet()) {
|
|
31
|
+
(extras.get(k) as? String)?.let { data[k] = it }
|
|
32
|
+
}
|
|
33
|
+
if (!PushMeshMessageRouter.isNossa(data)) return // pass-through: provedor legado segue
|
|
34
|
+
|
|
35
|
+
// ABORTAR ANTES do goAsync(). O goAsync() transfere o resultado do
|
|
36
|
+
// broadcast para o PendingResult; depois dele o receiver não tem mais
|
|
37
|
+
// resultado "pendente" e abortBroadcast() lança IllegalStateException —
|
|
38
|
+
// que num receiver do sistema NÃO é erro de push, é o app inteiro caindo
|
|
39
|
+
// (crash real no piloto, 2026-07-29). E só há o que abortar em broadcast
|
|
40
|
+
// ORDENADO: com o FCM ele é, mas a guarda evita depender disso.
|
|
41
|
+
if (isOrderedBroadcast) {
|
|
42
|
+
try {
|
|
43
|
+
abortBroadcast() // a mensagem é nossa: o SDK legado não a vê
|
|
44
|
+
} catch (_: Throwable) {
|
|
45
|
+
// sem resultado pendente (broadcast não ordenado): seguir sem abortar
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// NUNCA acessar reactNativeHost direto: na New Architecture o getter lança
|
|
50
|
+
// e, num receiver do sistema, isso derruba o app a cada push (2026-07-29).
|
|
51
|
+
val reactContext = PushMeshMessageRouter.contextReact(context)
|
|
52
|
+
// goAsync() + trabalho SÍNCRONO dentro da thread: o `finish()` só acontece
|
|
53
|
+
// depois que a exibição e o recibo terminaram DE VERDADE.
|
|
54
|
+
//
|
|
55
|
+
// POR QUE ISSO IMPORTA (corrigido em 2026-08-01): o goAsync só segura a
|
|
56
|
+
// prioridade do processo enquanto o resultado está pendente. Enquanto o
|
|
57
|
+
// `handle` apenas DISPARAVA threads soltas, o finish() saía em
|
|
58
|
+
// milissegundos e, com o app MORTO — o caso que este receiver existe para
|
|
59
|
+
// cobrir —, o processo virava "cached" na hora; sob pressão de memória
|
|
60
|
+
// (os mesmos OEMs agressivos que o SDK mede em `bateria_irrestrita`) o LMK
|
|
61
|
+
// podia matá-lo NO MEIO do download da imagem: push que nunca apareceu e
|
|
62
|
+
// recibo perdido, sem erro em lugar nenhum. O `handle` agora é bloqueante
|
|
63
|
+
// e cabe na janela do broadcast (~10s) por construção — ver o KDoc dele.
|
|
64
|
+
val pending = goAsync()
|
|
65
|
+
thread {
|
|
66
|
+
try {
|
|
67
|
+
PushMeshMessageRouter.handle(context.applicationContext, data, reactContext)
|
|
68
|
+
} catch (t: Throwable) {
|
|
69
|
+
// LEI do SDK: push nunca derruba o app do cliente. Exceção não tratada
|
|
70
|
+
// em thread própria mata o processo — aqui ela para.
|
|
71
|
+
android.util.Log.w("PushMesh", "falha ao processar push (app segue normal)", t)
|
|
72
|
+
} finally {
|
|
73
|
+
pending.finish()
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
package io.pushmesh.sdk
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.ReactContext
|
|
4
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
5
|
+
import com.google.firebase.messaging.FirebaseMessagingService
|
|
6
|
+
import com.google.firebase.messaging.RemoteMessage
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* FirebaseMessagingService do modo ZERO-CONFIG — caminho FALLBACK de entrega:
|
|
10
|
+
* quando a cadeia de broadcast passa pelo PushMeshMessagingReceiver (dual-run),
|
|
11
|
+
* é ele quem processa; este service cobre o cenário em que o FCM entrega
|
|
12
|
+
* direto ao service (sem o receiver na cadeia, comportamento de alguns OEMs).
|
|
13
|
+
*
|
|
14
|
+
* Com o corpo DATA-ONLY do servidor, onMessageReceived roda em TODOS os
|
|
15
|
+
* estados — tudo é delegado ao PushMeshMessageRouter (recibo direto +
|
|
16
|
+
* display NotificationCompat + evento/buffer JS), a MESMA lógica do receiver.
|
|
17
|
+
*
|
|
18
|
+
* onNewToken: emite 'PushMeshTokenRefresh' quando há JS e persiste o token
|
|
19
|
+
* pendente (SharedPreferences) — o boot reconcilia, então rotação de token
|
|
20
|
+
* não se perde. Exceção: no modo secundário o token NÃO é nosso (ver
|
|
21
|
+
* o comentário dentro do onNewToken).
|
|
22
|
+
*/
|
|
23
|
+
class PushMeshMessagingService : FirebaseMessagingService() {
|
|
24
|
+
|
|
25
|
+
// LEI do SDK: push nunca derruba o app do cliente. Estes dois callbacks
|
|
26
|
+
// rodam em thread do FCM — exceção que escapa daqui mata o processo
|
|
27
|
+
// inteiro (crash já visto em campo). Tudo fica dentro de guarda.
|
|
28
|
+
|
|
29
|
+
override fun onMessageReceived(message: RemoteMessage) {
|
|
30
|
+
val data = message.data
|
|
31
|
+
if (data.isEmpty()) return
|
|
32
|
+
// SEPARAÇÃO TOTAL (regra do produto, 2026-07-30): mensagem sem o NOSSO marcador
|
|
33
|
+
// é de outro SDK — não exibimos, não bufferizamos, não avisamos o JS. O
|
|
34
|
+
// receiver (caminho principal do dual-run) já deixa a cadeia seguir; aqui
|
|
35
|
+
// no fallback simplesmente não tocamos nela.
|
|
36
|
+
if (!PushMeshMessageRouter.isNossa(data)) return
|
|
37
|
+
try {
|
|
38
|
+
// handle() é BLOQUEANTE de propósito e aqui isso é o certo: onMessageReceived
|
|
39
|
+
// roda em thread de background do FCM, que mantém um wakelock enquanto não
|
|
40
|
+
// retorna. Voltar cedo (como fazia a versão com threads soltas) entregava o
|
|
41
|
+
// processo ao LMK no meio do download da imagem. O teto de ~9s do handle
|
|
42
|
+
// cabe na janela do serviço.
|
|
43
|
+
PushMeshMessageRouter.handle(applicationContext, data, currentReactContext())
|
|
44
|
+
} catch (t: Throwable) {
|
|
45
|
+
android.util.Log.w("PushMesh", "falha ao processar push (app segue normal)", t)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
override fun onNewToken(token: String) {
|
|
50
|
+
super.onNewToken(token)
|
|
51
|
+
try {
|
|
52
|
+
// ARMADILHA DO MULTI-PROJETO (corrigida em 2026-08-01): o onNewToken do
|
|
53
|
+
// FirebaseMessagingService só é chamado para o FirebaseApp **DEFAULT**.
|
|
54
|
+
// No fallback secundário — o host tem Firebase próprio e nós viramos o
|
|
55
|
+
// app nomeado 'pushmesh' (ver PushMeshFirebaseModule.configure) — o
|
|
56
|
+
// token que chega aqui é do PROJETO DO HOST. Registrá-lo como nosso
|
|
57
|
+
// quebra o player em silêncio: o servidor passa a enviar para um token
|
|
58
|
+
// de outro sender e todo envio falha com SENDER_ID_MISMATCH, sem erro
|
|
59
|
+
// visível para ninguém. A rotação do NOSSO token nesse modo já é coberta
|
|
60
|
+
// pelo getToken fresco que o boot faz em toda abertura do app.
|
|
61
|
+
if (PushMeshMessageRouter.modoFcm(applicationContext) == PushMeshMessageRouter.MODO_SECUNDARIO) {
|
|
62
|
+
android.util.Log.w(
|
|
63
|
+
"PushMesh",
|
|
64
|
+
"token novo ignorado: é do FirebaseApp default (do app host), não do nosso projeto — " +
|
|
65
|
+
"o boot revalida o token pelo app secundário",
|
|
66
|
+
)
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
PushMeshMessageRouter.salvarTokenPendente(applicationContext, token)
|
|
70
|
+
val reactContext = currentReactContext()
|
|
71
|
+
if (reactContext != null && reactContext.hasActiveReactInstance()) {
|
|
72
|
+
reactContext
|
|
73
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
74
|
+
.emit(PushMeshFirebaseModule.EVENT_TOKEN_REFRESH, token)
|
|
75
|
+
}
|
|
76
|
+
} catch (t: Throwable) {
|
|
77
|
+
android.util.Log.w("PushMesh", "falha ao tratar token novo (boot reconcilia)", t)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Delegado ao router: na New Architecture `reactNativeHost` LANÇA, e aqui
|
|
83
|
+
* estamos numa thread de serviço do FCM — a exceção derrubaria o app a
|
|
84
|
+
* cada push/rotação de token (crash real observado em campo).
|
|
85
|
+
*/
|
|
86
|
+
private fun currentReactContext(): ReactContext? =
|
|
87
|
+
PushMeshMessageRouter.contextReact(applicationContext)
|
|
88
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PushMeshConfig } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Sequência de boot do SDK: montagem automática do host in-app + init + hook de
|
|
4
|
+
* AppState + reporte de permissão no cold start (fire-and-forget — não bloqueia
|
|
5
|
+
* a abertura do app). Usado pelo PushMesh.init e pela API clássica (compat).
|
|
6
|
+
*/
|
|
7
|
+
export declare function boot(config: PushMeshConfig): Promise<void>;
|
package/dist/cjs/boot.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true}),exports.boot=boot;const auto_1=require("./inapp/auto"),permissions_1=require("./permissions"),players_1=require("./players");async function boot(config){true===config.inAppAutoMontar&&(0,auto_1.montarInAppAutomaticamente)(),await(0,players_1.init)(config),(0,permissions_1.attachAppStateHook)(),(0,permissions_1.checkAndReport)().catch(e=>console.warn("[PushMesh] checkAndReport falhou",e))}
|