@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
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
|
+
}
|