@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.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +160 -0
  3. package/android/build.gradle +82 -0
  4. package/android/src/main/AndroidManifest.xml +69 -0
  5. package/android/src/main/java/io/pushmesh/sdk/PushMeshClickActivity.kt +124 -0
  6. package/android/src/main/java/io/pushmesh/sdk/PushMeshFirebaseModule.kt +512 -0
  7. package/android/src/main/java/io/pushmesh/sdk/PushMeshFirebasePackage.kt +20 -0
  8. package/android/src/main/java/io/pushmesh/sdk/PushMeshMessageBuffer.kt +53 -0
  9. package/android/src/main/java/io/pushmesh/sdk/PushMeshMessageRouter.kt +477 -0
  10. package/android/src/main/java/io/pushmesh/sdk/PushMeshMessagingReceiver.kt +77 -0
  11. package/android/src/main/java/io/pushmesh/sdk/PushMeshMessagingService.kt +88 -0
  12. package/dist/cjs/boot.d.ts +7 -0
  13. package/dist/cjs/boot.js +1 -0
  14. package/dist/cjs/compat.d.ts +63 -0
  15. package/dist/cjs/compat.js +1 -0
  16. package/dist/cjs/doctor.d.ts +27 -0
  17. package/dist/cjs/doctor.js +2 -0
  18. package/dist/cjs/events.d.ts +21 -0
  19. package/dist/cjs/events.js +1 -0
  20. package/dist/cjs/http.d.ts +20 -0
  21. package/dist/cjs/http.js +1 -0
  22. package/dist/cjs/inapp/SkeletonShimmer.d.ts +1 -0
  23. package/dist/cjs/inapp/SkeletonShimmer.js +1 -0
  24. package/dist/cjs/inapp/auto.d.ts +1 -0
  25. package/dist/cjs/inapp/auto.js +1 -0
  26. package/dist/cjs/inapp/components.d.ts +14 -0
  27. package/dist/cjs/inapp/components.js +1 -0
  28. package/dist/cjs/inapp/engine.d.ts +1 -0
  29. package/dist/cjs/inapp/engine.js +1 -0
  30. package/dist/cjs/inapp/imageCache.d.ts +1 -0
  31. package/dist/cjs/inapp/imageCache.js +1 -0
  32. package/dist/cjs/inapp/index.d.ts +20 -0
  33. package/dist/cjs/inapp/index.js +1 -0
  34. package/dist/cjs/inapp/lifecycle.d.ts +1 -0
  35. package/dist/cjs/inapp/lifecycle.js +1 -0
  36. package/dist/cjs/inapp/normalize.d.ts +1 -0
  37. package/dist/cjs/inapp/normalize.js +1 -0
  38. package/dist/cjs/inapp/registro.d.ts +1 -0
  39. package/dist/cjs/inapp/registro.js +1 -0
  40. package/dist/cjs/inapp/sessao.d.ts +1 -0
  41. package/dist/cjs/inapp/sessao.js +1 -0
  42. package/dist/cjs/index.d.ts +118 -0
  43. package/dist/cjs/index.js +1 -0
  44. package/dist/cjs/perfil.d.ts +52 -0
  45. package/dist/cjs/perfil.js +1 -0
  46. package/dist/cjs/permissions.d.ts +54 -0
  47. package/dist/cjs/permissions.js +1 -0
  48. package/dist/cjs/players.d.ts +20 -0
  49. package/dist/cjs/players.js +1 -0
  50. package/dist/cjs/queue.d.ts +1 -0
  51. package/dist/cjs/queue.js +1 -0
  52. package/dist/cjs/receipts.d.ts +32 -0
  53. package/dist/cjs/receipts.js +1 -0
  54. package/dist/cjs/state.d.ts +21 -0
  55. package/dist/cjs/state.js +1 -0
  56. package/dist/cjs/storage.d.ts +1 -0
  57. package/dist/cjs/storage.js +1 -0
  58. package/dist/cjs/tags.d.ts +13 -0
  59. package/dist/cjs/tags.js +1 -0
  60. package/dist/cjs/triggers.d.ts +22 -0
  61. package/dist/cjs/triggers.js +1 -0
  62. package/dist/cjs/types.d.ts +258 -0
  63. package/dist/cjs/types.js +1 -0
  64. package/dist/cjs/zeroconfig.d.ts +45 -0
  65. package/dist/cjs/zeroconfig.js +1 -0
  66. package/ios/NotificationService/LEIA-ME.md +105 -0
  67. package/ios/NotificationService/PushMeshNotificationService.swift +384 -0
  68. package/ios/PushMeshApns.swift +746 -0
  69. package/ios/PushMeshApnsModule.m +76 -0
  70. package/ios/PushMeshSwizzleRegistro.swift +141 -0
  71. package/package.json +113 -0
  72. package/pushmesh-sdk.podspec +32 -0
  73. package/react-native.config.js +17 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 PushMesh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,160 @@
1
+ # @pushmesh/sdk
2
+
3
+ SDK React Native do **PushMesh** — push próprio com recibo real de entrega,
4
+ reativação de permissão (módulo Reativa) e banners in-app.
5
+
6
+ - TypeScript, build CJS único e **minificado com nomes preservados** (stack
7
+ trace legível no app, sem source map), `sideEffects: false`
8
+ - Compatível com New Architecture (Fabric) — testado contra RN 0.83
9
+ - Runtime: **fetch nativo** (sem axios) + `@notifee/react-native` + AsyncStorage
10
+ - **Dois modos**: zero-config (Android via FCM por código; iOS via APNs direto —
11
+ ambos **sem Firebase no app**) ou modo host (o token vem do app)
12
+ - Inclui **API clássica** (`/compat`), compatível com o formato de SDK de push
13
+ mais difundido do mercado — migração sem reescrever o app
14
+
15
+ > **Guia completo** (zero-config em detalhe, modo host, API clássica, rotas,
16
+ > imagem no iOS, permissões, caminho nativo): <https://docs.pushmesh.io> — o
17
+ > mesmo guia mora no repositório, em `sdk/docs/GUIA-INTEGRACAO.md`.
18
+ > · A doc de cada método vive no hover do editor (JSDoc embarcado nos tipos).
19
+
20
+ ## Instalação
21
+
22
+ ```bash
23
+ npm install @pushmesh/sdk @notifee/react-native @react-native-async-storage/async-storage
24
+ cd ios && bundle exec pod install # iOS
25
+ ```
26
+
27
+ ## Começo rápido (zero-config — recomendado)
28
+
29
+ Sem Firebase no app: cadastre o app no painel PushMesh (Android: cole o
30
+ `google-services.json` em Configurações → FCM; iOS: suba a chave `.p8` em
31
+ Configurações → APNs e habilite Push Notifications + Background Modes →
32
+ Remote notifications no Xcode). No app:
33
+
34
+ ```js
35
+ import PushMesh from '@pushmesh/sdk';
36
+
37
+ await PushMesh.init({
38
+ appId: 'UUID_DO_APP_NO_PUSHMESH', // SÓ ISSO é obrigatório
39
+ appVersion: '1.0.3',
40
+ // baseUrl é OPCIONAL — sem ela vale o servidor oficial
41
+ // (https://push.pushmesh.io). Passe uma URL só para self-host.
42
+ });
43
+ // no login do app: PushMesh.login(idDoUsuario);
44
+ ```
45
+
46
+ Banners in-app: uma linha — monte `<PushMeshInAppHost />` (exportado pelo
47
+ pacote) uma vez na raiz da sua árvore. Para o SDK se pendurar sozinho na raiz,
48
+ `init({ ..., inAppAutoMontar: true })` — automático é **opt-in**, porque ele
49
+ mexe no provedor de wrapper do React Native.
50
+
51
+ Registro, token, display, recibo real e clique funcionam sem mais nenhuma
52
+ linha. Imagem no push do iOS exige a Notification Service Extension — código
53
+ pronto e guia de 5 min em `ios/NotificationService/LEIA-ME.md`.
54
+
55
+ ## Modo host (app com Firebase próprio)
56
+
57
+ Quem já tem Firebase passa `getToken` e nada muda (o zero-config nem é acionado):
58
+
59
+ ```js
60
+ await PushMesh.init({
61
+ appId: 'UUID_DO_APP_NO_PUSHMESH',
62
+ getToken: () => messaging().getToken(),
63
+ });
64
+ messaging().onTokenRefresh(t => PushMesh.setToken(t));
65
+ messaging().onMessage(m => PushMesh.receipts.handlePushData(m.data));
66
+ messaging().setBackgroundMessageHandler(async m => { PushMesh.receipts.handlePushData(m.data); });
67
+ messaging().onNotificationOpenedApp(m => PushMesh.notifications.handleClick(m.data));
68
+ ```
69
+
70
+ ## API
71
+
72
+ `PushMesh.init` · `setToken` · `login` · `logout` · `getPlayerId` ·
73
+ `receipts.handlePushData` · `notifications.{handleClick,addEventListener,removeEventListener}` ·
74
+ `permissions.{checkAndReport,openNotificationSettings,canRequestNatively}` ·
75
+ `user.{addTag,addTags,removeTag,removeTags}` · `inapp.{pending,track}` ·
76
+ `definirGatilho`/`removerGatilho`/`limparGatilhos` (gatilho no aplicativo) ·
77
+ `triggers.*` · `doctor()`.
78
+
79
+ Cada método tem JSDoc completo no hover do editor; assinaturas e semântica no
80
+ [guia](https://docs.pushmesh.io). A **API clássica**
81
+ (initialize/Notifications/User/InAppMessages) vem no entry `@pushmesh/sdk/compat`.
82
+
83
+ ## Leve por contrato
84
+
85
+ O SDK roda no celular do cliente final — cada request e cada kB são orçados:
86
+
87
+ - **Zero polling** — rede só no init, no `AppState → active` e no push recebido;
88
+ - **Registro idempotente** — re-init com o mesmo payload = 1 request (provado em teste);
89
+ - **Fila offline educada** — backoff com jitter, `Retry-After` honrado, máx.
90
+ 10 tentativas e 5 requests concorrentes no flush (provado em teste);
91
+ - **Orçamento de pacote** — `npm run size` falha o build se o payload JS do
92
+ tarball passar do teto (o código nativo Android/iOS não conta — vira código
93
+ compilado, não bundle JS).
94
+
95
+ ## Diagnóstico
96
+
97
+ ```bash
98
+ npx pushmesh-doctor --base-url https://push.pushmesh.io \
99
+ --app-id <uuid> [--player-id <uuid>] [--api-key pm_live_...]
100
+ ```
101
+
102
+ Exit code `0` = tudo verde; cada falha sai com "como corrigir". Dentro do app,
103
+ `PushMesh.doctor()` imprime o equivalente on-device. Para testar sem push
104
+ real, use o device sandbox: `getToken: async () => 'test:ok'` (detalhes no guia).
105
+
106
+ ## Requisitos
107
+
108
+ | Item | Mínimo |
109
+ |---|---|
110
+ | React Native | 0.71 (New Architecture testada em 0.83) |
111
+ | React | 17 |
112
+ | Node (build/CLI) | 18 |
113
+ | iOS | 15.1 (alinhado ao mínimo do RN do app) |
114
+ | Android | conforme o `minSdk` do app |
115
+
116
+ Peer dependencies: `@notifee/react-native` ≥ 7 e
117
+ `@react-native-async-storage/async-storage` ≥ 1.17.
118
+
119
+ ## Desenvolvimento do SDK
120
+
121
+ ```bash
122
+ npm install
123
+ npm run typecheck # tsc --noEmit
124
+ npm test # jest com mocks (sem device)
125
+ npm run build # CJS minificado (nomes preservados) em dist/cjs
126
+ npm run test:types # tsd — contrato dos exports (roda DEPOIS do build)
127
+ npm run size # orçamento: payload JS ≤ 40 kB (nativo não conta)
128
+ ```
129
+
130
+ ## Licença
131
+
132
+ MIT — PushMesh 2026. Ver [LICENSE](LICENSE).
133
+
134
+ ---
135
+
136
+ ## English summary
137
+
138
+ React Native SDK for **PushMesh** — self-hosted-capable push with real delivery
139
+ receipts, permission re-activation and in-app banners.
140
+
141
+ ```bash
142
+ npm install @pushmesh/sdk @notifee/react-native @react-native-async-storage/async-storage
143
+ cd ios && bundle exec pod install
144
+ ```
145
+
146
+ ```js
147
+ import PushMesh from '@pushmesh/sdk';
148
+
149
+ await PushMesh.init({ appId: 'YOUR_APP_UUID' }); // App ID is the only required field
150
+ PushMesh.login(userId); // when your app logs the user in
151
+ ```
152
+
153
+ Zero-config mode needs **no Firebase SDK in your app**: Android gets FCM
154
+ credentials at runtime, iOS talks APNs directly with the `.p8` key you upload in
155
+ the dashboard. Already using Firebase? Pass `getToken` and keep your setup
156
+ (host mode). A drop-in **classic API** lives in `@pushmesh/sdk/compat` for
157
+ migrating from the most widespread push SDK format without rewriting your app.
158
+ Full documentation: <https://docs.pushmesh.io> — the same guide lives in this
159
+ repository at `sdk/docs/GUIA-INTEGRACAO.md` (Portuguese, with an English
160
+ summary at the end). MIT licensed.
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Módulo nativo Android do modo ZERO-CONFIG do PUSHMESH.
3
+ *
4
+ * Inicializa o Firebase por código (política default-first: FirebaseApp
5
+ * DEFAULT no caminho principal; secundário "pushmesh" só como fallback
6
+ * multi-projeto), com os parâmetros públicos vindos do servidor — SEM
7
+ * google-services.json e SEM o plugin com.google.gms.google-services no app
8
+ * do cliente (é justamente essa exigência que o modo zero-config elimina).
9
+ *
10
+ * compileSdk/minSdk/kotlin seguem o rootProject do app host, com fallbacks.
11
+ */
12
+ buildscript {
13
+ def kotlin_version = rootProject.ext.has('kotlinVersion') ? rootProject.ext.get('kotlinVersion') : '1.9.24'
14
+ repositories {
15
+ google()
16
+ mavenCentral()
17
+ }
18
+ dependencies {
19
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
20
+ }
21
+ }
22
+
23
+ def safeExtGet(String prop, Object fallback) {
24
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
25
+ }
26
+
27
+ apply plugin: 'com.android.library'
28
+ apply plugin: 'kotlin-android'
29
+
30
+ android {
31
+ namespace "io.pushmesh.sdk"
32
+ compileSdkVersion safeExtGet('compileSdkVersion', 35)
33
+
34
+ defaultConfig {
35
+ minSdkVersion safeExtGet('minSdkVersion', 24)
36
+ targetSdkVersion safeExtGet('targetSdkVersion', 35)
37
+ }
38
+
39
+ buildTypes {
40
+ release {
41
+ minifyEnabled false
42
+ }
43
+ }
44
+
45
+ compileOptions {
46
+ sourceCompatibility JavaVersion.VERSION_17
47
+ targetCompatibility JavaVersion.VERSION_17
48
+ }
49
+ }
50
+
51
+ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
52
+ kotlinOptions {
53
+ jvmTarget = "17"
54
+ }
55
+ }
56
+
57
+ repositories {
58
+ google()
59
+ mavenCentral()
60
+ }
61
+
62
+ dependencies {
63
+ // React Native vem do app host (autolinking).
64
+ implementation "com.facebook.react:react-android"
65
+
66
+ // NotificationCompat (display data-only): garantida em qualquer app RN.
67
+ implementation "androidx.core:core:1.13.1"
68
+
69
+ // Firebase Cloud Messaging via BoM.
70
+ //
71
+ // PIN PROPOSITAL: BoM 34.14.1 → firebase-messaging 25.0.2, a última linha
72
+ // ANTES da transição token→FID (a 25.1.0, no BoM 34.15.0, deprecou
73
+ // getToken/onNewToken e o suporte multi-projeto do FCM — PR
74
+ // firebase-android-sdk#8087). A migração para o modelo FID é planejada no
75
+ // backend — ver HANDOFF seção 2026-07-28. Não subir o BoM sem essa migração.
76
+ // Mapeamento confirmado nos POMs dos BoMs (dl.google.com/dl/android/maven2).
77
+ //
78
+ // SEM o plugin google-services: o FirebaseApp é inicializado por código
79
+ // (default-first, ver PushMeshFirebaseModule).
80
+ implementation platform("com.google.firebase:firebase-bom:34.14.1")
81
+ implementation "com.google.firebase:firebase-messaging"
82
+ }
@@ -0,0 +1,69 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+
3
+ <!--
4
+ Android 13+ (API 33): sem esta permissão o sistema NÃO exibe notificação
5
+ nenhuma — e a falha é silenciosa. Declarada AQUI para o app do cliente não
6
+ precisar saber disso (é a promessa do zero-config). Em app que já usa outro
7
+ SDK de push a declaração é idêntica e o manifest merger apenas funde as duas.
8
+ Pedir a permissão ao usuário continua sendo do app host (ou do SDK legado no
9
+ dual-run) — o SDK só lê e reporta o estado (PushMesh.permissions).
10
+ -->
11
+ <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
12
+
13
+ <application>
14
+ <!--
15
+ DUAL-RUN (migração do provedor legado): receiver de alta prioridade que
16
+ captura SÓ as mensagens nossas (marcador pmx_* — abortBroadcast) e deixa
17
+ as demais seguirem a cadeia para o SDK do provedor legado.
18
+ Ver PushMeshMessagingReceiver.kt.
19
+ -->
20
+ <receiver
21
+ android:name="io.pushmesh.sdk.PushMeshMessagingReceiver"
22
+ android:exported="true"
23
+ android:permission="com.google.android.c2dm.permission.SEND">
24
+ <intent-filter android:priority="999">
25
+ <action android:name="com.google.android.c2dm.intent.RECEIVE" />
26
+ </intent-filter>
27
+ </receiver>
28
+
29
+ <!--
30
+ Toque na notificação: trampolim INVISÍVEL.
31
+
32
+ Tem de ser Activity, não BroadcastReceiver: desde o Android 10 app em
33
+ segundo plano não pode abrir tela, então um PendingIntent de broadcast
34
+ teria o startActivity BLOQUEADO EM SILÊNCIO e o toque não faria nada
35
+ (sintoma real do piloto, 2026-07-30). PendingIntent de Activity é
36
+ iniciado pelo sistema e pode abrir o destino.
37
+
38
+ Ela registra o clique e abre a `pm_url` (ou o app, quando não há URL).
39
+ Translucent + excludeFromRecents + taskAffinity vazio: nunca aparece na
40
+ tela nem na lista de recentes, e não se mistura à task do app.
41
+ Não exportada: só o nosso PendingIntent a dispara.
42
+ -->
43
+ <activity
44
+ android:name="io.pushmesh.sdk.PushMeshClickActivity"
45
+ android:exported="false"
46
+ android:excludeFromRecents="true"
47
+ android:noHistory="true"
48
+ android:taskAffinity=""
49
+ android:theme="@android:style/Theme.Translucent.NoTitleBar" />
50
+
51
+ <!--
52
+ Caminho fallback de entrega (ver PushMeshMessagingService.kt — o
53
+ receiver acima é o principal). Se o app host já tem o PRÓPRIO
54
+ FirebaseMessagingService (modo host), apenas um service recebe
55
+ MESSAGING_EVENT — nesse caso o host pode remover este via manifest merger:
56
+ <service android:name="io.pushmesh.sdk.PushMeshMessagingService"
57
+ tools:node="remove" />
58
+ O modo host não usa nada deste módulo nativo.
59
+ -->
60
+ <service
61
+ android:name="io.pushmesh.sdk.PushMeshMessagingService"
62
+ android:exported="false">
63
+ <intent-filter>
64
+ <action android:name="com.google.firebase.MESSAGING_EVENT" />
65
+ </intent-filter>
66
+ </service>
67
+ </application>
68
+
69
+ </manifest>
@@ -0,0 +1,124 @@
1
+ package io.pushmesh.sdk
2
+
3
+ import android.app.Activity
4
+ import android.app.NotificationManager
5
+ import android.content.Context
6
+ import android.content.Intent
7
+ import android.net.Uri
8
+ import android.os.Bundle
9
+
10
+ /**
11
+ * Trampolim do toque na notificação — Activity invisível.
12
+ *
13
+ * POR QUE UMA ACTIVITY, e não um BroadcastReceiver:
14
+ * desde o Android 10 um app em segundo plano **não pode abrir tela**. Um
15
+ * `PendingIntent` de broadcast roda com o app em background, então o
16
+ * `startActivity` dele é bloqueado EM SILÊNCIO — o usuário toca na notificação
17
+ * e nada acontece (foi exatamente o sintoma no piloto, 2026-07-30). Já um
18
+ * `PendingIntent` de ACTIVITY é iniciado pelo próprio sistema e, estando em
19
+ * primeiro plano, pode abrir o que quiser. É por isso que os SDKs do mercado
20
+ * abrem o app e só então redirecionam.
21
+ *
22
+ * Sequência: registra o clique (Kotlin, sem depender de JS vivo) → abre o
23
+ * destino → se apaga. Nunca aparece na tela nem na lista de recentes.
24
+ *
25
+ * Destino:
26
+ * 1. `pm_url` → abre a URL (http/https no navegador; esquema próprio cai no
27
+ * deep link do app, se ele declarar o intent-filter);
28
+ * 2. sem `pm_url` → abre o app com os dados como extras (o JS recebe em
29
+ * `getInitialNotification`/`onNewIntent`).
30
+ */
31
+ class PushMeshClickActivity : Activity() {
32
+
33
+ override fun onCreate(savedInstanceState: Bundle?) {
34
+ super.onCreate(savedInstanceState)
35
+ // LEI do SDK: push nunca derruba o app do cliente.
36
+ try {
37
+ val data = HashMap<String, String>()
38
+ intent?.extras?.let { extras ->
39
+ for (k in extras.keySet()) {
40
+ (extras.get(k) as? String)?.let { data[k] = it }
41
+ }
42
+ }
43
+
44
+ // 1) o clique é contado ANTES de qualquer navegação
45
+ PushMeshMessageRouter.postarRecibo(applicationContext, data, "clique")
46
+
47
+ // 2) tira a notificação da bandeja
48
+ runCatching {
49
+ data["pm_msg_id"]?.hashCode()?.let { id ->
50
+ (getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager)?.cancel(id)
51
+ }
52
+ }
53
+
54
+ // 3) destino
55
+ val url = data["pm_url"]?.trim().orEmpty()
56
+ val abriu = if (url.isNotEmpty()) abrirUrl(url, data) else false
57
+ if (!abriu) abrirApp(data)
58
+ } catch (t: Throwable) {
59
+ android.util.Log.w("PushMesh", "falha ao tratar clique", t)
60
+ runCatching { abrirApp(emptyMap()) }
61
+ } finally {
62
+ // Sem transição visual: o usuário não vê tela intermediária nenhuma.
63
+ finish()
64
+ overridePendingTransition(0, 0)
65
+ }
66
+ }
67
+
68
+ /** true = alguém aceitou a URL; false = cai para o app. */
69
+ private fun abrirUrl(url: String, data: Map<String, String>): Boolean = try {
70
+ val i = Intent(Intent.ACTION_VIEW, Uri.parse(url)).apply {
71
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
72
+ }
73
+ // LEI (privacidade do payload): os extras do push só viajam quando quem
74
+ // abre a URL é o PRÓPRIO app. O payload carrega `pm_rcpt` — a prova do
75
+ // recibo daquela mensagem —, `pm_msg_id` e todo bloco `pm_*`; anexá-los a
76
+ // um ACTION_VIEW implícito entrega o pacote inteiro a QUEM resolver a URL:
77
+ // o navegador ou qualquer app que declare intent-filter para o
78
+ // esquema/host (registrar esquema alheio é trivial no Android). Dado do
79
+ // nosso contrato não entra em processo de terceiro de graça — e cada campo
80
+ // novo do payload herda esta regra sem ninguém precisar lembrar.
81
+ // Degradação combinada: em destino externo o JS não recebe os extras, mas
82
+ // o clique JÁ foi contado acima (postarRecibo) e a navegação acontece
83
+ // igual — perde-se contexto, nunca a função.
84
+ if (ehDoProprioApp(i)) {
85
+ for ((k, v) in data) i.putExtra(k, v)
86
+ }
87
+ startActivity(i)
88
+ true
89
+ } catch (_: Throwable) {
90
+ // sem navegador ou esquema que ninguém trata: melhor abrir o app do que
91
+ // deixar o toque sem efeito
92
+ false
93
+ }
94
+
95
+ /**
96
+ * O destino da URL é o PRÓPRIO app (deep link nosso)?
97
+ *
98
+ * Resolve o intent como o sistema resolveria e compara o vencedor com o
99
+ * nosso `packageName`. O intent segue IMPLÍCITO de propósito: com
100
+ * `setPackage` forçaríamos o app mesmo quando o sistema mandaria para o
101
+ * navegador (App Link não verificado), mudando o destino do toque — aqui só
102
+ * decidimos o que ANEXAR, nunca para onde ir.
103
+ *
104
+ * Filtro de visibilidade de pacotes (Android 11+) joga a favor: o próprio
105
+ * pacote é SEMPRE visível, então deep link nosso resolve certo; navegador de
106
+ * terceiro pode voltar `null` sem `<queries>` e cai no ramo seguro (sem
107
+ * extras). Chooser do sistema (vários candidatos) também não é o nosso
108
+ * pacote ⇒ sem extras. Na dúvida, o intent vai limpo.
109
+ */
110
+ private fun ehDoProprioApp(intent: Intent): Boolean = runCatching {
111
+ packageManager.resolveActivity(intent, 0)?.activityInfo?.packageName == packageName
112
+ }.getOrDefault(false)
113
+
114
+ private fun abrirApp(data: Map<String, String>) {
115
+ val launch = packageManager.getLaunchIntentForPackage(packageName) ?: return
116
+ for ((k, v) in data) launch.putExtra(k, v)
117
+ launch.addFlags(
118
+ Intent.FLAG_ACTIVITY_NEW_TASK or
119
+ Intent.FLAG_ACTIVITY_SINGLE_TOP or
120
+ Intent.FLAG_ACTIVITY_CLEAR_TOP,
121
+ )
122
+ runCatching { startActivity(launch) }
123
+ }
124
+ }