@pushmesh/sdk 0.7.2 → 0.7.3-rc.3
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/README.md +104 -93
- package/README.pt-BR.md +163 -0
- package/app.plugin.js +8 -0
- package/dist/cjs/boot.js +1 -1
- package/dist/cjs/doctor.d.ts +2 -2
- package/dist/cjs/doctor.js +1 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/perfil.d.ts +1 -1
- package/dist/cjs/perfil.js +1 -1
- package/dist/cjs/permissions.d.ts +17 -0
- package/dist/cjs/permissions.js +1 -1
- package/dist/cjs/state.d.ts +1 -1
- package/dist/cjs/state.js +1 -1
- package/dist/cjs/types.d.ts +17 -1
- package/ios/NotificationService/README.md +108 -0
- package/package.json +18 -5
- package/plugin/index.js +226 -0
package/README.md
CHANGED
|
@@ -1,64 +1,95 @@
|
|
|
1
1
|
# @pushmesh/sdk
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
React Native SDK for **PushMesh** — push notifications with **real delivery
|
|
4
|
+
receipts**, permission re-activation and in-app banners.
|
|
5
5
|
|
|
6
|
-
|
|
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
|
|
6
|
+
> 🇧🇷 **Este guia também existe em português:** [README.pt-BR.md](./README.pt-BR.md)
|
|
14
7
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
8
|
+
- TypeScript, single CJS build, **minified with identifiers preserved** (readable
|
|
9
|
+
stack traces in your app, no source map needed), `sideEffects: false`
|
|
10
|
+
- New Architecture (Fabric) compatible — tested against RN 0.83
|
|
11
|
+
- Runtime: **native fetch** (no axios) + `@notifee/react-native` + AsyncStorage
|
|
12
|
+
- **Two modes**: zero-config (Android over FCM by code; iOS over APNs directly —
|
|
13
|
+
both **without Firebase in your app**) or host mode (your app owns the token)
|
|
14
|
+
- Ships a **classic API** (`/compat`) matching the most widespread push SDK
|
|
15
|
+
format on the market — migrate without rewriting your app
|
|
19
16
|
|
|
20
|
-
|
|
17
|
+
> **Full guide** (zero-config in depth, host mode, classic API, routes, iOS
|
|
18
|
+
> images, permissions, the native path): <https://docs.pushmesh.io>
|
|
19
|
+
> · Per-method documentation lives in your editor's hover (JSDoc ships with the
|
|
20
|
+
> types).
|
|
21
|
+
|
|
22
|
+
## Install
|
|
21
23
|
|
|
22
24
|
```bash
|
|
23
25
|
npm install @pushmesh/sdk @notifee/react-native @react-native-async-storage/async-storage
|
|
24
26
|
cd ios && bundle exec pod install # iOS
|
|
25
27
|
```
|
|
26
28
|
|
|
27
|
-
##
|
|
29
|
+
## Expo — zero native setup
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npx expo install @pushmesh/sdk @notifee/react-native @react-native-async-storage/async-storage
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{ "expo": { "plugins": [["@pushmesh/sdk", { "modo": "producao" }]] } }
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Then `npx expo prebuild`. The config plugin does the iOS work Apple requires and
|
|
40
|
+
nothing else: the Push Notifications capability, `remote-notification` background
|
|
41
|
+
mode, and the **Notification Service Extension** — the only way an iPhone can
|
|
42
|
+
download a push image. That extension is normally a five-minute manual dance in
|
|
43
|
+
Xcode, repeated after every prebuild; here it is generated, idempotent, and it
|
|
44
|
+
inherits your app's iOS deployment target (an extension built for a newer iOS
|
|
45
|
+
silently never runs on older devices).
|
|
46
|
+
|
|
47
|
+
**Android needs no plugin at all** — and that is the point. The native module
|
|
48
|
+
already declares `POST_NOTIFICATIONS`, the receiver and the click trampoline, and
|
|
49
|
+
zero-config fetches Firebase parameters at runtime. **Your app never carries
|
|
50
|
+
`google-services.json`**, so no credential file in your repo.
|
|
51
|
+
|
|
52
|
+
Plugin options: `modo` (`"desenvolvimento"` default | `"producao"` — TestFlight
|
|
53
|
+
and the App Store both need `producao`), `imagemNoPush` (default `true`),
|
|
54
|
+
`iconeNotificacao`, `corNotificacao`.
|
|
28
55
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
56
|
+
## Quick start (zero-config — recommended)
|
|
57
|
+
|
|
58
|
+
No Firebase SDK in your app. Register the app in the PushMesh dashboard
|
|
59
|
+
(Android: paste `google-services.json` under Settings → FCM; iOS: upload your
|
|
60
|
+
`.p8` key under Settings → APNs, and enable Push Notifications plus Background
|
|
61
|
+
Modes → Remote notifications in Xcode). Then, in your app:
|
|
33
62
|
|
|
34
63
|
```js
|
|
35
64
|
import PushMesh from '@pushmesh/sdk';
|
|
36
65
|
|
|
37
66
|
await PushMesh.init({
|
|
38
|
-
appId: '
|
|
67
|
+
appId: 'YOUR_APP_UUID_IN_PUSHMESH', // the ONLY required field
|
|
39
68
|
appVersion: '1.0.3',
|
|
40
|
-
// baseUrl
|
|
41
|
-
// (https://
|
|
69
|
+
// baseUrl is OPTIONAL — without it the official server is used
|
|
70
|
+
// (https://api.pushmesh.io). Pass a URL only when self-hosting.
|
|
42
71
|
});
|
|
43
|
-
//
|
|
72
|
+
// when your app logs the user in: PushMesh.login(userId);
|
|
44
73
|
```
|
|
45
74
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
`init({ ..., inAppAutoMontar: true })` —
|
|
49
|
-
|
|
75
|
+
In-app banners are one line: mount `<PushMeshInAppHost />` (exported by the
|
|
76
|
+
package) once at the root of your tree. To let the SDK mount itself at the root,
|
|
77
|
+
use `init({ ..., inAppAutoMontar: true })` — automatic mounting is **opt-in**,
|
|
78
|
+
because it touches React Native's wrapper provider.
|
|
50
79
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
80
|
+
Registration, token handling, display, real receipts and click handling all work
|
|
81
|
+
with no further code. Images in iOS pushes require the Notification Service
|
|
82
|
+
Extension — ready-made code and a 5-minute guide in
|
|
83
|
+
[`ios/NotificationService/README.md`](./ios/NotificationService/README.md).
|
|
54
84
|
|
|
55
|
-
##
|
|
85
|
+
## Host mode (app with its own Firebase)
|
|
56
86
|
|
|
57
|
-
|
|
87
|
+
If you already have Firebase, pass `getToken` and nothing else changes
|
|
88
|
+
(zero-config never kicks in):
|
|
58
89
|
|
|
59
90
|
```js
|
|
60
91
|
await PushMesh.init({
|
|
61
|
-
appId: '
|
|
92
|
+
appId: 'YOUR_APP_UUID_IN_PUSHMESH',
|
|
62
93
|
getToken: () => messaging().getToken(),
|
|
63
94
|
});
|
|
64
95
|
messaging().onTokenRefresh(t => PushMesh.setToken(t));
|
|
@@ -73,88 +104,68 @@ messaging().onNotificationOpenedApp(m => PushMesh.notifications.handleClick(m.da
|
|
|
73
104
|
`receipts.handlePushData` · `notifications.{handleClick,addEventListener,removeEventListener}` ·
|
|
74
105
|
`permissions.{checkAndReport,openNotificationSettings,canRequestNatively}` ·
|
|
75
106
|
`user.{addTag,addTags,removeTag,removeTags}` · `inapp.{pending,track}` ·
|
|
76
|
-
`definirGatilho`/`removerGatilho`/`limparGatilhos` (
|
|
107
|
+
`definirGatilho`/`removerGatilho`/`limparGatilhos` (in-app triggers) ·
|
|
77
108
|
`triggers.*` · `doctor()`.
|
|
78
109
|
|
|
79
|
-
|
|
80
|
-
[
|
|
81
|
-
(initialize/Notifications/User/InAppMessages)
|
|
110
|
+
Every method carries full JSDoc on hover; signatures and semantics are in the
|
|
111
|
+
[guide](https://docs.pushmesh.io). The **classic API**
|
|
112
|
+
(initialize/Notifications/User/InAppMessages) ships in the `@pushmesh/sdk/compat`
|
|
113
|
+
entry point.
|
|
82
114
|
|
|
83
|
-
##
|
|
115
|
+
## Light by contract
|
|
84
116
|
|
|
85
|
-
|
|
117
|
+
This SDK runs on your end users' phones — every request and every kB is
|
|
118
|
+
budgeted:
|
|
86
119
|
|
|
87
|
-
- **Zero polling** —
|
|
88
|
-
|
|
89
|
-
- **
|
|
90
|
-
|
|
91
|
-
- **
|
|
92
|
-
|
|
93
|
-
|
|
120
|
+
- **Zero polling** — the network is touched only on init, on `AppState → active`
|
|
121
|
+
and when a push arrives;
|
|
122
|
+
- **Idempotent registration** — re-init with the same payload = 1 request
|
|
123
|
+
(proven by test);
|
|
124
|
+
- **Polite offline queue** — backoff with jitter, `Retry-After` honoured, at most
|
|
125
|
+
10 attempts and 5 concurrent requests per flush (proven by test);
|
|
126
|
+
- **Package budget** — `npm run size` fails the build if the tarball's JS payload
|
|
127
|
+
exceeds the ceiling (Android/iOS native code does not count — it becomes
|
|
128
|
+
compiled code, not JS bundle).
|
|
94
129
|
|
|
95
|
-
##
|
|
130
|
+
## Diagnostics
|
|
96
131
|
|
|
97
132
|
```bash
|
|
98
|
-
npx pushmesh-doctor --base-url https://
|
|
133
|
+
npx pushmesh-doctor --base-url https://api.pushmesh.io \
|
|
99
134
|
--app-id <uuid> [--player-id <uuid>] [--api-key pm_live_...]
|
|
100
135
|
```
|
|
101
136
|
|
|
102
|
-
Exit code `0`
|
|
103
|
-
`PushMesh.doctor()`
|
|
104
|
-
real, use
|
|
137
|
+
Exit code `0` means everything is green; each failure prints how to fix it.
|
|
138
|
+
Inside the app, `PushMesh.doctor()` prints the on-device equivalent. To test
|
|
139
|
+
without a real push, use the device sandbox: `getToken: async () => 'test:ok'`
|
|
140
|
+
(details in the guide).
|
|
141
|
+
|
|
142
|
+
> The CLI and the SDK's runtime messages currently speak Portuguese. The API,
|
|
143
|
+
> the types and this documentation are English.
|
|
105
144
|
|
|
106
|
-
##
|
|
145
|
+
## Requirements
|
|
107
146
|
|
|
108
|
-
| Item |
|
|
147
|
+
| Item | Minimum |
|
|
109
148
|
|---|---|
|
|
110
|
-
| React Native | 0.71 (New Architecture
|
|
149
|
+
| React Native | 0.71 (New Architecture tested on 0.83) |
|
|
111
150
|
| React | 17 |
|
|
112
151
|
| Node (build/CLI) | 18 |
|
|
113
|
-
| iOS | 15.1 (
|
|
114
|
-
| Android |
|
|
152
|
+
| iOS | 15.1 (matching your app's RN minimum) |
|
|
153
|
+
| Android | whatever your app's `minSdk` is |
|
|
115
154
|
|
|
116
|
-
Peer dependencies: `@notifee/react-native` ≥ 7
|
|
155
|
+
Peer dependencies: `@notifee/react-native` ≥ 7 and
|
|
117
156
|
`@react-native-async-storage/async-storage` ≥ 1.17.
|
|
118
157
|
|
|
119
|
-
##
|
|
158
|
+
## Developing the SDK
|
|
120
159
|
|
|
121
160
|
```bash
|
|
122
161
|
npm install
|
|
123
162
|
npm run typecheck # tsc --noEmit
|
|
124
|
-
npm test # jest
|
|
125
|
-
npm run build # CJS
|
|
126
|
-
npm run test:types # tsd —
|
|
127
|
-
npm run size #
|
|
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
|
|
163
|
+
npm test # jest with mocks (no device needed)
|
|
164
|
+
npm run build # minified CJS (identifiers preserved) into dist/cjs
|
|
165
|
+
npm run test:types # tsd — export contract (runs AFTER the build)
|
|
166
|
+
npm run size # budget: JS payload ≤ 40 kB (native does not count)
|
|
144
167
|
```
|
|
145
168
|
|
|
146
|
-
|
|
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
|
-
```
|
|
169
|
+
## License
|
|
152
170
|
|
|
153
|
-
|
|
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.
|
|
171
|
+
MIT — PushMesh 2026. See [LICENSE](LICENSE).
|
package/README.pt-BR.md
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# @pushmesh/sdk
|
|
2
|
+
|
|
3
|
+
SDK React Native do **PushMesh** — push com **recibo real de entrega**,
|
|
4
|
+
reativação de permissão (módulo Reativa) e banners in-app.
|
|
5
|
+
|
|
6
|
+
> 🇺🇸 **This guide is also available in English:** [README.md](./README.md)
|
|
7
|
+
|
|
8
|
+
- TypeScript, build CJS único e **minificado com nomes preservados** (stack
|
|
9
|
+
trace legível no app, sem source map), `sideEffects: false`
|
|
10
|
+
- Compatível com New Architecture (Fabric) — testado contra RN 0.83
|
|
11
|
+
- Runtime: **fetch nativo** (sem axios) + `@notifee/react-native` + AsyncStorage
|
|
12
|
+
- **Dois modos**: zero-config (Android via FCM por código; iOS via APNs direto —
|
|
13
|
+
ambos **sem Firebase no app**) ou modo host (o token vem do app)
|
|
14
|
+
- Inclui **API clássica** (`/compat`), compatível com o formato de SDK de push
|
|
15
|
+
mais difundido do mercado — migração sem reescrever o app
|
|
16
|
+
|
|
17
|
+
> **Guia completo** (zero-config em detalhe, modo host, API clássica, rotas,
|
|
18
|
+
> imagem no iOS, permissões, caminho nativo): <https://docs.pushmesh.io> — o
|
|
19
|
+
> mesmo guia mora no repositório, em `sdk/docs/GUIA-INTEGRACAO.md`.
|
|
20
|
+
> · A doc de cada método vive no hover do editor (JSDoc embarcado nos tipos).
|
|
21
|
+
|
|
22
|
+
## Instalação
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install @pushmesh/sdk @notifee/react-native @react-native-async-storage/async-storage
|
|
26
|
+
cd ios && bundle exec pod install # iOS
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Expo — sem tocar em código nativo
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npx expo install @pushmesh/sdk @notifee/react-native @react-native-async-storage/async-storage
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{ "expo": { "plugins": [["@pushmesh/sdk", { "modo": "producao" }]] } }
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Depois, `npx expo prebuild`. O plugin faz o que a Apple obriga e nada além: a
|
|
40
|
+
capability Push Notifications, o modo `remote-notification` e a **Notification
|
|
41
|
+
Service Extension** — a única forma de o iPhone baixar a imagem de um push. Essa
|
|
42
|
+
extensão normalmente são cinco minutos no Xcode, refeitos a cada prebuild; aqui
|
|
43
|
+
ela é gerada, é idempotente, e herda o iOS mínimo do SEU app (extensão criada
|
|
44
|
+
num iOS mais novo simplesmente não roda nos aparelhos antigos — sem erro
|
|
45
|
+
nenhum).
|
|
46
|
+
|
|
47
|
+
**O Android não precisa de plugin** — e é justamente aí que está a vantagem: o
|
|
48
|
+
módulo nativo já declara `POST_NOTIFICATIONS`, o receiver e o trampolim de
|
|
49
|
+
clique, e o zero-config busca os parâmetros do Firebase em tempo de execução.
|
|
50
|
+
**O seu app nunca carrega o `google-services.json`**, então nenhum arquivo de
|
|
51
|
+
credencial entra no repositório.
|
|
52
|
+
|
|
53
|
+
Opções: `modo` (`"desenvolvimento"` padrão | `"producao"` — TestFlight e App
|
|
54
|
+
Store exigem `producao`), `imagemNoPush` (padrão `true`), `iconeNotificacao`,
|
|
55
|
+
`corNotificacao`.
|
|
56
|
+
|
|
57
|
+
## Começo rápido (zero-config — recomendado)
|
|
58
|
+
|
|
59
|
+
Sem Firebase no app: cadastre o app no painel PushMesh (Android: cole o
|
|
60
|
+
`google-services.json` em Configurações → FCM; iOS: suba a chave `.p8` em
|
|
61
|
+
Configurações → APNs e habilite Push Notifications + Background Modes →
|
|
62
|
+
Remote notifications no Xcode). No app:
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
import PushMesh from '@pushmesh/sdk';
|
|
66
|
+
|
|
67
|
+
await PushMesh.init({
|
|
68
|
+
appId: 'UUID_DO_APP_NO_PUSHMESH', // SÓ ISSO é obrigatório
|
|
69
|
+
appVersion: '1.0.3',
|
|
70
|
+
// baseUrl é OPCIONAL — sem ela vale o servidor oficial
|
|
71
|
+
// (https://api.pushmesh.io). Passe uma URL só para self-host.
|
|
72
|
+
});
|
|
73
|
+
// no login do app: PushMesh.login(idDoUsuario);
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Banners in-app: uma linha — monte `<PushMeshInAppHost />` (exportado pelo
|
|
77
|
+
pacote) uma vez na raiz da sua árvore. Para o SDK se pendurar sozinho na raiz,
|
|
78
|
+
`init({ ..., inAppAutoMontar: true })` — automático é **opt-in**, porque ele
|
|
79
|
+
mexe no provedor de wrapper do React Native.
|
|
80
|
+
|
|
81
|
+
Registro, token, display, recibo real e clique funcionam sem mais nenhuma
|
|
82
|
+
linha. Imagem no push do iOS exige a Notification Service Extension — código
|
|
83
|
+
pronto e guia de 5 min em
|
|
84
|
+
[`ios/NotificationService/LEIA-ME.md`](./ios/NotificationService/LEIA-ME.md).
|
|
85
|
+
|
|
86
|
+
## Modo host (app com Firebase próprio)
|
|
87
|
+
|
|
88
|
+
Quem já tem Firebase passa `getToken` e nada muda (o zero-config nem é acionado):
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
await PushMesh.init({
|
|
92
|
+
appId: 'UUID_DO_APP_NO_PUSHMESH',
|
|
93
|
+
getToken: () => messaging().getToken(),
|
|
94
|
+
});
|
|
95
|
+
messaging().onTokenRefresh(t => PushMesh.setToken(t));
|
|
96
|
+
messaging().onMessage(m => PushMesh.receipts.handlePushData(m.data));
|
|
97
|
+
messaging().setBackgroundMessageHandler(async m => { PushMesh.receipts.handlePushData(m.data); });
|
|
98
|
+
messaging().onNotificationOpenedApp(m => PushMesh.notifications.handleClick(m.data));
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## API
|
|
102
|
+
|
|
103
|
+
`PushMesh.init` · `setToken` · `login` · `logout` · `getPlayerId` ·
|
|
104
|
+
`receipts.handlePushData` · `notifications.{handleClick,addEventListener,removeEventListener}` ·
|
|
105
|
+
`permissions.{checkAndReport,openNotificationSettings,canRequestNatively}` ·
|
|
106
|
+
`user.{addTag,addTags,removeTag,removeTags}` · `inapp.{pending,track}` ·
|
|
107
|
+
`definirGatilho`/`removerGatilho`/`limparGatilhos` (gatilho no aplicativo) ·
|
|
108
|
+
`triggers.*` · `doctor()`.
|
|
109
|
+
|
|
110
|
+
Cada método tem JSDoc completo no hover do editor; assinaturas e semântica no
|
|
111
|
+
[guia](https://docs.pushmesh.io). A **API clássica**
|
|
112
|
+
(initialize/Notifications/User/InAppMessages) vem no entry `@pushmesh/sdk/compat`.
|
|
113
|
+
|
|
114
|
+
## Leve por contrato
|
|
115
|
+
|
|
116
|
+
O SDK roda no celular do cliente final — cada request e cada kB são orçados:
|
|
117
|
+
|
|
118
|
+
- **Zero polling** — rede só no init, no `AppState → active` e no push recebido;
|
|
119
|
+
- **Registro idempotente** — re-init com o mesmo payload = 1 request (provado em teste);
|
|
120
|
+
- **Fila offline educada** — backoff com jitter, `Retry-After` honrado, máx.
|
|
121
|
+
10 tentativas e 5 requests concorrentes no flush (provado em teste);
|
|
122
|
+
- **Orçamento de pacote** — `npm run size` falha o build se o payload JS do
|
|
123
|
+
tarball passar do teto (o código nativo Android/iOS não conta — vira código
|
|
124
|
+
compilado, não bundle JS).
|
|
125
|
+
|
|
126
|
+
## Diagnóstico
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
npx pushmesh-doctor --base-url https://api.pushmesh.io \
|
|
130
|
+
--app-id <uuid> [--player-id <uuid>] [--api-key pm_live_...]
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Exit code `0` = tudo verde; cada falha sai com "como corrigir". Dentro do app,
|
|
134
|
+
`PushMesh.doctor()` imprime o equivalente on-device. Para testar sem push
|
|
135
|
+
real, use o device sandbox: `getToken: async () => 'test:ok'` (detalhes no guia).
|
|
136
|
+
|
|
137
|
+
## Requisitos
|
|
138
|
+
|
|
139
|
+
| Item | Mínimo |
|
|
140
|
+
|---|---|
|
|
141
|
+
| React Native | 0.71 (New Architecture testada em 0.83) |
|
|
142
|
+
| React | 17 |
|
|
143
|
+
| Node (build/CLI) | 18 |
|
|
144
|
+
| iOS | 15.1 (alinhado ao mínimo do RN do app) |
|
|
145
|
+
| Android | conforme o `minSdk` do app |
|
|
146
|
+
|
|
147
|
+
Peer dependencies: `@notifee/react-native` ≥ 7 e
|
|
148
|
+
`@react-native-async-storage/async-storage` ≥ 1.17.
|
|
149
|
+
|
|
150
|
+
## Desenvolvimento do SDK
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
npm install
|
|
154
|
+
npm run typecheck # tsc --noEmit
|
|
155
|
+
npm test # jest com mocks (sem device)
|
|
156
|
+
npm run build # CJS minificado (nomes preservados) em dist/cjs
|
|
157
|
+
npm run test:types # tsd — contrato dos exports (roda DEPOIS do build)
|
|
158
|
+
npm run size # orçamento: payload JS ≤ 40 kB (nativo não conta)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Licença
|
|
162
|
+
|
|
163
|
+
MIT — PushMesh 2026. Ver [LICENSE](LICENSE).
|
package/app.plugin.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ponto de entrada do plugin de configuração do Expo.
|
|
3
|
+
*
|
|
4
|
+
* O Expo procura EXATAMENTE `app.plugin.js` na raiz do pacote quando alguém
|
|
5
|
+
* escreve `"plugins": ["@pushmesh/sdk"]`. É só o encaminhamento — a
|
|
6
|
+
* implementação mora em `plugin/index.js`.
|
|
7
|
+
*/
|
|
8
|
+
module.exports = require('./plugin');
|
package/dist/cjs/boot.js
CHANGED
|
@@ -1 +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))}
|
|
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){if(true===config.inAppAutoMontar&&(0,auto_1.montarInAppAutomaticamente)(),await(0,players_1.init)(config),(0,permissions_1.attachAppStateHook)(),true===config.pedirPermissao)try{await(0,permissions_1.reativarNotificacoes)()}catch(e){console.warn("[PushMesh] pedirPermissao falhou",e)}(0,permissions_1.checkAndReport)().then(permissions_1.avisarSeMudo).catch(e=>console.warn("[PushMesh] checkAndReport falhou",e))}
|
package/dist/cjs/doctor.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* (a lei do erro que ensina).
|
|
8
8
|
*
|
|
9
9
|
* Uso:
|
|
10
|
-
* npx pushmesh-doctor --base-url https://
|
|
10
|
+
* npx pushmesh-doctor --base-url https://api.pushmesh.io \
|
|
11
11
|
* --app-id <uuid> [--player-id <uuid>] [--api-key pm_live_...] [--timeout 8000]
|
|
12
12
|
*
|
|
13
13
|
* Exit code: 0 = tudo verde · 1 = ao menos uma falha.
|
|
@@ -23,5 +23,5 @@ declare function fetchJson(url: string, timeoutMs: number, headers?: Record<stri
|
|
|
23
23
|
status: number;
|
|
24
24
|
json: any;
|
|
25
25
|
}>;
|
|
26
|
-
declare const USO = "pushmesh-doctor \u2014 diagn\u00F3stico da integra\u00E7\u00E3o PushMesh.\n\nUso:\n npx pushmesh-doctor --base-url <url> --app-id <uuid> [op\u00E7\u00F5es]\n\nObrigat\u00F3rios:\n --base-url <url> servidor PushMesh (oficial: https://
|
|
26
|
+
declare const USO = "pushmesh-doctor \u2014 diagn\u00F3stico da integra\u00E7\u00E3o PushMesh.\n\nUso:\n npx pushmesh-doctor --base-url <url> --app-id <uuid> [op\u00E7\u00F5es]\n\nObrigat\u00F3rios:\n --base-url <url> servidor PushMesh (oficial: https://api.pushmesh.io)\n --app-id <uuid> App ID do app no painel\n\nOpcionais:\n --player-id <uuid> confere um aparelho j\u00E1 registrado\n --api-key pm_live_\u2026 destrava as checagens que exigem autentica\u00E7\u00E3o\n --timeout <ms> timeout por requisi\u00E7\u00E3o (padr\u00E3o: 8000)\n -h, --help mostra esta ajuda\n\nExit code: 0 = tudo verde \u00B7 1 = ao menos uma falha.\nDoc: https://docs.pushmesh.io";
|
|
27
27
|
declare function main(): Promise<void>;
|
package/dist/cjs/doctor.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
"use strict";function parseArgs(argv){const args={};for(let i=0;i<argv.length;i++){const a=argv[i];if(a.startsWith("--")){const key=a.slice(2),next=argv[i+1];next&&!next.startsWith("--")?(args[key]=next,i++):args[key]="true"}}return args}async function fetchJson(url,timeoutMs,headers){const controller=new AbortController,timer=setTimeout(()=>controller.abort(),timeoutMs);try{const res=await fetch(url,{headers:headers,signal:controller.signal}),text=await res.text();let json=null;try{json=JSON.parse(text)}catch{json=text}return{status:res.status,json:json}}finally{clearTimeout(timer)}}const USO="pushmesh-doctor — diagnóstico da integração PushMesh.\n\nUso:\n npx pushmesh-doctor --base-url <url> --app-id <uuid> [opções]\n\nObrigatórios:\n --base-url <url> servidor PushMesh (oficial: https://
|
|
2
|
+
"use strict";function parseArgs(argv){const args={};for(let i=0;i<argv.length;i++){const a=argv[i];if(a.startsWith("--")){const key=a.slice(2),next=argv[i+1];next&&!next.startsWith("--")?(args[key]=next,i++):args[key]="true"}}return args}async function fetchJson(url,timeoutMs,headers){const controller=new AbortController,timer=setTimeout(()=>controller.abort(),timeoutMs);try{const res=await fetch(url,{headers:headers,signal:controller.signal}),text=await res.text();let json=null;try{json=JSON.parse(text)}catch{json=text}return{status:res.status,json:json}}finally{clearTimeout(timer)}}const USO="pushmesh-doctor — diagnóstico da integração PushMesh.\n\nUso:\n npx pushmesh-doctor --base-url <url> --app-id <uuid> [opções]\n\nObrigatórios:\n --base-url <url> servidor PushMesh (oficial: https://api.pushmesh.io)\n --app-id <uuid> App ID do app no painel\n\nOpcionais:\n --player-id <uuid> confere um aparelho já registrado\n --api-key pm_live_… destrava as checagens que exigem autenticação\n --timeout <ms> timeout por requisição (padrão: 8000)\n -h, --help mostra esta ajuda\n\nExit code: 0 = tudo verde · 1 = ao menos uma falha.\nDoc: https://docs.pushmesh.io";async function main(){const argv=process.argv.slice(2);(0===argv.length||argv.includes("--help")||argv.includes("-h"))&&(console.log(USO),process.exit(0));const args=parseArgs(argv),checks=[],baseUrl=(args["base-url"]||"").replace(/\/+$/,"");baseUrl||(console.error("FALHA: --base-url é obrigatório.\n Como corrigir: npx pushmesh-doctor --base-url https://api.pushmesh.io --app-id <uuid>"),process.exit(1));const timeoutMs=Number(args.timeout||8e3);let health=null;try{const res=await fetchJson(`${baseUrl}/health`,timeoutMs);health=res.json,200===res.status&&health&&"ok"===health.status?checks.push({nome:"baseUrl alcançável + /health ok",ok:true,detalhe:`versao=${health.versao??"?"} papel=${health.papel??"?"} pg=${health.pg??"?"} valkey=${health.valkey??"?"}`}):checks.push({nome:"baseUrl alcançável + /health ok",ok:false,detalhe:`HTTP ${res.status}`,comoCorrigir:'o /health do PUSHMESH deve responder 200 {"status":"ok"}. Verifique se o serviço está no ar (systemctl status pushmesh) e se o LB está healthy.'})}catch(e){checks.push({nome:"baseUrl alcançável + /health ok",ok:false,detalhe:String(e),comoCorrigir:"confira a URL (sem barra final), DNS/TLS do domínio e se esta máquina alcança o servidor (VPN/firewall). "})}health&&"ok"===health.status&&("ok"!==health.pg&&checks.push({nome:"Postgres saudável",ok:false,detalhe:`pg=${health.pg} pg_down_ha_s=${health.pg_down_ha_s??"?"}`,comoCorrigir:"O banco do serviço está fora do ar e a API responde 503 por contrato — não é a integração do seu app. Aguarde o restabelecimento ou fale com o suporte."}),"ok"!==health.valkey&&checks.push({nome:"Valkey saudável",ok:null,detalhe:`valkey=${health.valkey} — o serviço está em modo degradado, mas segue entregando`}));const appId=args["app-id"],playerId=args["player-id"],apiKey=args["api-key"];if(playerId&&!apiKey)checks.push({nome:"player registrado",ok:null,detalhe:"pulado: GET /players/:id exige --api-key (rota de servidor, Basic auth)"});else if(playerId&&apiKey){appId||(console.error("FALHA: --app-id é obrigatório junto com --player-id."),process.exit(1));try{const res=await fetchJson(`${baseUrl}/api/v1/players/${encodeURIComponent(playerId)}?app_id=${encodeURIComponent(appId)}`,timeoutMs,{Authorization:`Basic ${apiKey}`});if(200===res.status&&res.json){const p=res.json;checks.push({nome:"player registrado",ok:true,detalhe:`id=${playerId}`}),checks.push(p.identifier?{nome:"token de push presente",ok:true,detalhe:"identifier preenchido"}:{nome:"token de push presente",ok:false,detalhe:"identifier vazio",comoCorrigir:"o device se registrou sem token. No app, confira config.getToken / PushMesh.setToken e o google-services.json."});const nt=p.notification_types;checks.push(1===nt?{nome:"permissão de notificação",ok:true,detalhe:"notification_types=1 (ativo)"}:{nome:"permissão de notificação",ok:0===nt&&null,detalhe:`notification_types=${nt}`,comoCorrigir:-2===nt?"usuário bloqueou. Use a campanha Reativa: botão open_notification_settings + detecção de virada no foreground.":"permissão nunca solicitada. canRequestNatively()=true: dá para pedir o prompt nativo (spec §4)."}),p.invalid_identifier&&checks.push({nome:"token válido no FCM/APNs",ok:false,detalhe:"invalid_identifier=true (token morto — UNREGISTERED)",comoCorrigir:"o app precisa abrir uma vez para re-registrar com token novo (upsert por app_id+token)."})}else 404===res.status?checks.push({nome:"player registrado",ok:false,detalhe:`404 — player ${playerId} não existe nesse tenant`,comoCorrigir:"confira app_id e player_id (o player_id é o devices.uuid devolvido pelo POST /players, guardado no AsyncStorage pm:player_id)."}):401===res.status?checks.push({nome:"player registrado",ok:false,detalhe:"401 — api_key ausente/revogada ou app_id ≠ app da chave",comoCorrigir:"confira a api_key do app; se rotacionou, a antiga segue válida por 24h."}):checks.push({nome:"player registrado",ok:false,detalhe:`HTTP ${res.status}: ${JSON.stringify(res.json)}`})}catch(e){checks.push({nome:"player registrado",ok:false,detalhe:String(e),comoCorrigir:"falha de rede consultando o player — veja o item baseUrl acima."})}}console.log(`\npushmesh-doctor — ${baseUrl}\n${"=".repeat(60)}`);let falhas=0;for(const c of checks){const marca=true===c.ok?"OK ":null===c.ok?"INFO ":"FALHA";false===c.ok&&falhas++,console.log(`${marca} ${c.nome}: ${c.detalhe}`),c.comoCorrigir&&console.log(` → como corrigir: ${c.comoCorrigir}`)}console.log("=".repeat(60)),console.log(0===falhas?"Tudo verde.":`${falhas} falha(s) — corrija na ordem acima.`),process.exit(0===falhas?0:1)}main().catch(e=>{console.error(`pushmesh-doctor quebrou: ${String(e)}`),process.exit(1)});
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ import { definirGatilho, definirGatilhos, lerGatilhos, limparGatilhos, removerGa
|
|
|
22
22
|
export declare const PushMesh: {
|
|
23
23
|
/**
|
|
24
24
|
* Inicializa o SDK — uma vez no boot do app. SÓ o `appId` é obrigatório:
|
|
25
|
-
* sem `baseUrl` vale o servidor oficial (https://
|
|
25
|
+
* sem `baseUrl` vale o servidor oficial (https://api.pushmesh.io).
|
|
26
26
|
* Chamadas concorrentes compartilham o mesmo boot (um registro só).
|
|
27
27
|
*/
|
|
28
28
|
init: typeof boot;
|
package/dist/cjs/perfil.d.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* linha sozinho (script `version` → scripts/sync-versao.mjs); editando à mão,
|
|
21
21
|
* suba as duas.
|
|
22
22
|
*/
|
|
23
|
-
export declare const SDK_VERSAO = "0.7.
|
|
23
|
+
export declare const SDK_VERSAO = "0.7.3-rc.3";
|
|
24
24
|
export interface PerfilDispositivo {
|
|
25
25
|
device_os?: string;
|
|
26
26
|
fabricante?: string;
|
package/dist/cjs/perfil.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"__esModule",{value:true}),exports.SDK_VERSAO=void 0,exports.coletarPerfil=coletarPerfil;const react_native_1=require("react-native"),storage_1=require("./storage");function moduloPerfil(){const mods=react_native_1.NativeModules;return"ios"===react_native_1.Platform.OS?mods.PushMeshApns:mods.PushMeshFirebase}function timezoneId(){try{return Intl.DateTimeFormat().resolvedOptions().timeZone||void 0}catch{return}}function pais(){try{const loc=Intl.DateTimeFormat().resolvedOptions().locale,m=/[-_]([A-Za-z]{2})(?:[-_]|$)/.exec(loc);return m?m[1].toUpperCase():void 0}catch{return}}async function proximaSessao(){const atual=Number(await(0,storage_1.getItem)(storage_1.KEYS.sessoes)??"0"),proxima=Number.isFinite(atual)&&atual>=0?atual+1:1;return await(0,storage_1.setItem)(storage_1.KEYS.sessoes,String(proxima)),proxima}function semVazios(p){const out={};for(const[k,v]of Object.entries(p))null!=v&&""!==v&&(out[k]=v);return out}async function coletarPerfil(){const perfil={sdk_versao:exports.SDK_VERSAO,timezone_id:timezoneId(),pais:pais()};try{perfil.sessoes=await proximaSessao()}catch{}try{const nativo=moduloPerfil();if(nativo?.perfilDispositivo){const n=await nativo.perfilDispositivo();Object.assign(perfil,{device_os:n.device_os,fabricante:n.fabricante,modelo:n.modelo,net_type:n.net_type,carrier:n.carrier,rooted:n.rooted,standby_bucket:n.standby_bucket,bateria_irrestrita:n.bateria_irrestrita,notif_permissoes:n.notif_permissoes,instalacao_id:n.instalacao_id})}}catch{}if(!perfil.device_os){const v=react_native_1.Platform.Version;perfil.device_os="string"==typeof v?v:String(v)}if(!perfil.fabricante){const c=react_native_1.Platform.constants;perfil.fabricante="ios"===react_native_1.Platform.OS?"Apple":c?.Manufacturer}return semVazios(perfil)}exports.SDK_VERSAO="0.7.
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true}),exports.SDK_VERSAO=void 0,exports.coletarPerfil=coletarPerfil;const react_native_1=require("react-native"),storage_1=require("./storage");function moduloPerfil(){const mods=react_native_1.NativeModules;return"ios"===react_native_1.Platform.OS?mods.PushMeshApns:mods.PushMeshFirebase}function timezoneId(){try{return Intl.DateTimeFormat().resolvedOptions().timeZone||void 0}catch{return}}function pais(){try{const loc=Intl.DateTimeFormat().resolvedOptions().locale,m=/[-_]([A-Za-z]{2})(?:[-_]|$)/.exec(loc);return m?m[1].toUpperCase():void 0}catch{return}}async function proximaSessao(){const atual=Number(await(0,storage_1.getItem)(storage_1.KEYS.sessoes)??"0"),proxima=Number.isFinite(atual)&&atual>=0?atual+1:1;return await(0,storage_1.setItem)(storage_1.KEYS.sessoes,String(proxima)),proxima}function semVazios(p){const out={};for(const[k,v]of Object.entries(p))null!=v&&""!==v&&(out[k]=v);return out}async function coletarPerfil(){const perfil={sdk_versao:exports.SDK_VERSAO,timezone_id:timezoneId(),pais:pais()};try{perfil.sessoes=await proximaSessao()}catch{}try{const nativo=moduloPerfil();if(nativo?.perfilDispositivo){const n=await nativo.perfilDispositivo();Object.assign(perfil,{device_os:n.device_os,fabricante:n.fabricante,modelo:n.modelo,net_type:n.net_type,carrier:n.carrier,rooted:n.rooted,standby_bucket:n.standby_bucket,bateria_irrestrita:n.bateria_irrestrita,notif_permissoes:n.notif_permissoes,instalacao_id:n.instalacao_id})}}catch{}if(!perfil.device_os){const v=react_native_1.Platform.Version;perfil.device_os="string"==typeof v?v:String(v)}if(!perfil.fabricante){const c=react_native_1.Platform.constants;perfil.fabricante="ios"===react_native_1.Platform.OS?"Apple":c?.Manufacturer}return semVazios(perfil)}exports.SDK_VERSAO="0.7.3-rc.3";
|
|
@@ -52,3 +52,20 @@ export declare function markReativaPending(messageId: string): Promise<void>;
|
|
|
52
52
|
export declare function attachAppStateHook(): void;
|
|
53
53
|
/** Remove o hook (hot reload / testes). */
|
|
54
54
|
export declare function detachAppStateHook(): void;
|
|
55
|
+
/**
|
|
56
|
+
* Grita quando o aparelho está MUDO — o defeito mais caro deste produto, e o
|
|
57
|
+
* mais silencioso.
|
|
58
|
+
*
|
|
59
|
+
* Sem a permissão, tudo dá "certo": o aparelho registra, o disparo responde
|
|
60
|
+
* `successful: 1`, o FCM aceita — e nada aparece na tela, e `recebidos` fica em
|
|
61
|
+
* zero para sempre. No Android 12 ou anterior isso quase não acontecia (a
|
|
62
|
+
* permissão vinha concedida de fábrica); a partir do Android 13 ela virou
|
|
63
|
+
* pedido em tempo de execução, então o app que nunca pede é um app cujo push
|
|
64
|
+
* nunca chega — e o integrador só descobre em produção, olhando um painel que
|
|
65
|
+
* diz "entregue".
|
|
66
|
+
*
|
|
67
|
+
* O SDK não pode pedir a permissão sozinho (ver `pedirPermissao` em types.ts),
|
|
68
|
+
* mas pode se recusar a ficar calado. Uma linha de console, uma vez por boot,
|
|
69
|
+
* dizendo o que fazer.
|
|
70
|
+
*/
|
|
71
|
+
export declare function avisarSeMudo(tipos: RpNotificationTypes): void;
|
package/dist/cjs/permissions.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var __createBinding=this&&this.__createBinding||(Object.create?function(o,m,k,k2){void 0===k2&&(k2=k);var desc=Object.getOwnPropertyDescriptor(m,k);desc&&!("get"in desc?!m.__esModule:desc.writable||desc.configurable)||(desc={enumerable:true,get:function(){return m[k]}}),Object.defineProperty(o,k2,desc)}:function(o,m,k,k2){void 0===k2&&(k2=k),o[k2]=m[k]}),__setModuleDefault=this&&this.__setModuleDefault||(Object.create?function(o,v){Object.defineProperty(o,"default",{enumerable:true,value:v})}:function(o,v){o.default=v}),__importStar=this&&this.__importStar||function(){var ownKeys=function(o){return ownKeys=Object.getOwnPropertyNames||function(o){var ar=[];for(var k in o)Object.prototype.hasOwnProperty.call(o,k)&&(ar[ar.length]=k);return ar},ownKeys(o)};return function(mod){if(mod&&mod.__esModule)return mod;var result={};if(null!=mod)for(var k=ownKeys(mod),i=0;i<k.length;i++)"default"!==k[i]&&__createBinding(result,mod,k[i]);return __setModuleDefault(result,mod),result}}();Object.defineProperty(exports,"__esModule",{value:true}),exports.mapAuthorizationStatus=mapAuthorizationStatus,exports.readNotificationTypes=readNotificationTypes,exports.canRequestNatively=canRequestNatively,exports.openNotificationSettings=openNotificationSettings,exports.reativarNotificacoes=reativarNotificacoes,exports.checkAndReport=checkAndReport,exports.markReativaPending=markReativaPending,exports.attachAppStateHook=attachAppStateHook,exports.detachAppStateHook=detachAppStateHook;const react_native_1=__importStar(require("@notifee/react-native")),react_native_2=require("react-native"),queue_1=require("./queue"),state_1=require("./state"),storage_1=require("./storage"),types_1=require("./types");function mapAuthorizationStatus(status){switch(status){case react_native_1.AuthorizationStatus.AUTHORIZED:case react_native_1.AuthorizationStatus.PROVISIONAL:return types_1.RpNotificationTypes.Ativo;case react_native_1.AuthorizationStatus.DENIED:return types_1.RpNotificationTypes.Bloqueado;case react_native_1.AuthorizationStatus.NOT_DETERMINED:default:return types_1.RpNotificationTypes.NuncaPerguntou}}async function readNotificationTypes(){return mapAuthorizationStatus((await react_native_1.default.getNotificationSettings()).authorizationStatus)}async function canRequestNatively(){return(await react_native_1.default.getNotificationSettings()).authorizationStatus===react_native_1.AuthorizationStatus.NOT_DETERMINED&&("ios"===react_native_2.Platform.OS||"android"===react_native_2.Platform.OS&&Number(react_native_2.Platform.Version)>=33)}async function openNotificationSettings(){if("android"===react_native_2.Platform.OS)return void await react_native_1.default.openNotificationSettings();const nativo=react_native_2.NativeModules.PushMeshApns;if(nativo?.abrirAjustesDeNotificacao)try{if(await nativo.abrirAjustesDeNotificacao())return}catch{}await react_native_2.Linking.openSettings()}async function reativarNotificacoes(){return await canRequestNatively()?await react_native_1.default.requestPermission():await openNotificationSettings(),1===await checkAndReport()}async function checkAndReport(){const current=await readNotificationTypes(),previousRaw=await(0,storage_1.getItem)(storage_1.KEYS.notificationTypes),previous=null===previousRaw?null:Number(previousRaw);if(previous===current)return current;const playerId=state_1.state.playerId??await(0,storage_1.getItem)(storage_1.KEYS.playerId);if(playerId&&state_1.state.config){const{sendOrQueue:sendOrQueue}=await Promise.resolve().then(()=>__importStar(require("./queue")));await sendOrQueue("PUT",`/api/v1/players/${playerId}`,{app_id:(0,state_1.requireConfig)().appId,notification_types:current}),await(0,storage_1.setItem)(storage_1.KEYS.notificationTypes,String(current))}if(current===types_1.RpNotificationTypes.Ativo&&null!==previous&&previous!==types_1.RpNotificationTypes.Ativo){const reativaPending=await(0,storage_1.getItem)(storage_1.KEYS.reativaPending);if(reativaPending){const{track:track}=await Promise.resolve().then(()=>__importStar(require("./inapp")));await track("reativou",reativaPending),await(0,storage_1.setItem)(storage_1.KEYS.reativaPending,"")}}return current}async function markReativaPending(messageId){await(0,storage_1.setItem)(storage_1.KEYS.reativaPending,messageId)}function attachAppStateHook(){state_1.state.appStateSubscription||(state_1.state.appStateSubscription=react_native_2.AppState.addEventListener("change",nextState=>{if("active"===nextState){const bgDesde=state_1.state.emBackgroundDesde;state_1.state.emBackgroundDesde=null,checkAndReport().catch(e=>console.warn("[PushMesh] checkAndReport falhou",e)),(0,queue_1.flushQueue)().catch(()=>{}),Promise.resolve().then(()=>__importStar(require("./inapp/lifecycle"))).then(m=>m.aoVoltarAoForeground(bgDesde)).catch(()=>{})}else null===state_1.state.emBackgroundDesde&&(state_1.state.emBackgroundDesde=Date.now())}))}function detachAppStateHook(){state_1.state.appStateSubscription?.remove(),state_1.state.appStateSubscription=null}
|
|
1
|
+
"use strict";var __createBinding=this&&this.__createBinding||(Object.create?function(o,m,k,k2){void 0===k2&&(k2=k);var desc=Object.getOwnPropertyDescriptor(m,k);desc&&!("get"in desc?!m.__esModule:desc.writable||desc.configurable)||(desc={enumerable:true,get:function(){return m[k]}}),Object.defineProperty(o,k2,desc)}:function(o,m,k,k2){void 0===k2&&(k2=k),o[k2]=m[k]}),__setModuleDefault=this&&this.__setModuleDefault||(Object.create?function(o,v){Object.defineProperty(o,"default",{enumerable:true,value:v})}:function(o,v){o.default=v}),__importStar=this&&this.__importStar||function(){var ownKeys=function(o){return ownKeys=Object.getOwnPropertyNames||function(o){var ar=[];for(var k in o)Object.prototype.hasOwnProperty.call(o,k)&&(ar[ar.length]=k);return ar},ownKeys(o)};return function(mod){if(mod&&mod.__esModule)return mod;var result={};if(null!=mod)for(var k=ownKeys(mod),i=0;i<k.length;i++)"default"!==k[i]&&__createBinding(result,mod,k[i]);return __setModuleDefault(result,mod),result}}();Object.defineProperty(exports,"__esModule",{value:true}),exports.mapAuthorizationStatus=mapAuthorizationStatus,exports.readNotificationTypes=readNotificationTypes,exports.canRequestNatively=canRequestNatively,exports.openNotificationSettings=openNotificationSettings,exports.reativarNotificacoes=reativarNotificacoes,exports.checkAndReport=checkAndReport,exports.markReativaPending=markReativaPending,exports.attachAppStateHook=attachAppStateHook,exports.detachAppStateHook=detachAppStateHook,exports.avisarSeMudo=avisarSeMudo;const react_native_1=__importStar(require("@notifee/react-native")),react_native_2=require("react-native"),queue_1=require("./queue"),state_1=require("./state"),storage_1=require("./storage"),types_1=require("./types");function mapAuthorizationStatus(status){switch(status){case react_native_1.AuthorizationStatus.AUTHORIZED:case react_native_1.AuthorizationStatus.PROVISIONAL:return types_1.RpNotificationTypes.Ativo;case react_native_1.AuthorizationStatus.DENIED:return types_1.RpNotificationTypes.Bloqueado;case react_native_1.AuthorizationStatus.NOT_DETERMINED:default:return types_1.RpNotificationTypes.NuncaPerguntou}}async function readNotificationTypes(){return mapAuthorizationStatus((await react_native_1.default.getNotificationSettings()).authorizationStatus)}async function canRequestNatively(){return(await react_native_1.default.getNotificationSettings()).authorizationStatus===react_native_1.AuthorizationStatus.NOT_DETERMINED&&("ios"===react_native_2.Platform.OS||"android"===react_native_2.Platform.OS&&Number(react_native_2.Platform.Version)>=33)}async function openNotificationSettings(){if("android"===react_native_2.Platform.OS)return void await react_native_1.default.openNotificationSettings();const nativo=react_native_2.NativeModules.PushMeshApns;if(nativo?.abrirAjustesDeNotificacao)try{if(await nativo.abrirAjustesDeNotificacao())return}catch{}await react_native_2.Linking.openSettings()}async function reativarNotificacoes(){return await canRequestNatively()?await react_native_1.default.requestPermission():await openNotificationSettings(),1===await checkAndReport()}async function checkAndReport(){const current=await readNotificationTypes(),previousRaw=await(0,storage_1.getItem)(storage_1.KEYS.notificationTypes),previous=null===previousRaw?null:Number(previousRaw);if(previous===current)return current;const playerId=state_1.state.playerId??await(0,storage_1.getItem)(storage_1.KEYS.playerId);if(playerId&&state_1.state.config){const{sendOrQueue:sendOrQueue}=await Promise.resolve().then(()=>__importStar(require("./queue")));await sendOrQueue("PUT",`/api/v1/players/${playerId}`,{app_id:(0,state_1.requireConfig)().appId,notification_types:current}),await(0,storage_1.setItem)(storage_1.KEYS.notificationTypes,String(current))}if(current===types_1.RpNotificationTypes.Ativo&&null!==previous&&previous!==types_1.RpNotificationTypes.Ativo){const reativaPending=await(0,storage_1.getItem)(storage_1.KEYS.reativaPending);if(reativaPending){const{track:track}=await Promise.resolve().then(()=>__importStar(require("./inapp")));await track("reativou",reativaPending),await(0,storage_1.setItem)(storage_1.KEYS.reativaPending,"")}}return current}async function markReativaPending(messageId){await(0,storage_1.setItem)(storage_1.KEYS.reativaPending,messageId)}function attachAppStateHook(){state_1.state.appStateSubscription||(state_1.state.appStateSubscription=react_native_2.AppState.addEventListener("change",nextState=>{if("active"===nextState){const bgDesde=state_1.state.emBackgroundDesde;state_1.state.emBackgroundDesde=null,checkAndReport().catch(e=>console.warn("[PushMesh] checkAndReport falhou",e)),(0,queue_1.flushQueue)().catch(()=>{}),Promise.resolve().then(()=>__importStar(require("./inapp/lifecycle"))).then(m=>m.aoVoltarAoForeground(bgDesde)).catch(()=>{})}else null===state_1.state.emBackgroundDesde&&(state_1.state.emBackgroundDesde=Date.now())}))}function detachAppStateHook(){state_1.state.appStateSubscription?.remove(),state_1.state.appStateSubscription=null}function avisarSeMudo(tipos){if(1===tipos)return;const comoResolver="chame await PushMesh.permissions.reativarNotificacoes() no momento certo do seu fluxo, ou passe { pedirPermissao: true } no init para pedir logo no boot.";console.warn(0===tipos?`[PushMesh] Este aparelho está com a notificação BLOQUEADA. O push vai ser aceito pelo FCM/APNs e NÃO vai aparecer na tela — e o recibo de entrega nunca chega. Como resolver: ${comoResolver}`:`[PushMesh] A permissão de notificação ainda não foi pedida a esta pessoa (Android 13+ e iOS exigem o pedido). Enquanto isso o push é entregue e não aparece. Como resolver: ${comoResolver}`)}
|
package/dist/cjs/state.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { PushMeshConfig } from './types';
|
|
|
6
6
|
* como override para servidor próprio ou para apontar num servidor local
|
|
7
7
|
* durante o desenvolvimento.
|
|
8
8
|
*/
|
|
9
|
-
export declare const BASE_URL_PADRAO = "https://
|
|
9
|
+
export declare const BASE_URL_PADRAO = "https://api.pushmesh.io";
|
|
10
10
|
export declare function requireConfig(): PushMeshConfig;
|
|
11
11
|
/** baseUrl sem barra final, para montar rotas sem '//'. */
|
|
12
12
|
export declare function baseUrl(): string;
|
package/dist/cjs/state.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";function __resetForTests(){exports.state.appStateSubscription&&exports.state.appStateSubscription.remove(),exports.state.config=null,exports.state.playerId=null,exports.state.token=null,exports.state.externalUserId=null,exports.state.appStateSubscription=null,exports.state.emBackgroundDesde=null,exports.state.flushing=false,exports.state.initEmVoo=null,exports.state.logoutPendente=false,exports.state.transporte=null}function requireConfig(){if(!exports.state.config)throw new Error("[PushMesh] init() ainda não foi chamado. Como corrigir: chame PushMesh.init({ appId }) no boot do app.");return exports.state.config}function baseUrl(){return(requireConfig().baseUrl??exports.BASE_URL_PADRAO).replace(/\/+$/,"")}function emDesenvolvimento(){return"undefined"!=typeof __DEV__&&true===__DEV__}function validarBaseUrl(url){const limpa=url.trim();if(!(/^https:\/\//i.test(limpa)||/^http:\/\/(localhost|127\.0\.0\.1|10\.0\.2\.2|10\.0\.3\.2|\[::1\])(:\d+)?(\/|$)/i.test(limpa)&&emDesenvolvimento()))throw new Error("[PushMesh] baseUrl precisa ser https:// — pelo corpo das requisições trafegam o token de push e o external_user_id do usuário. Como corrigir: use https no servidor (http só é aceito para localhost em builds de desenvolvimento).")}Object.defineProperty(exports,"__esModule",{value:true}),exports.state=exports.BASE_URL_PADRAO=void 0,exports.__resetForTests=__resetForTests,exports.requireConfig=requireConfig,exports.baseUrl=baseUrl,exports.emDesenvolvimento=emDesenvolvimento,exports.validarBaseUrl=validarBaseUrl,exports.BASE_URL_PADRAO="https://
|
|
1
|
+
"use strict";function __resetForTests(){exports.state.appStateSubscription&&exports.state.appStateSubscription.remove(),exports.state.config=null,exports.state.playerId=null,exports.state.token=null,exports.state.externalUserId=null,exports.state.appStateSubscription=null,exports.state.emBackgroundDesde=null,exports.state.flushing=false,exports.state.initEmVoo=null,exports.state.logoutPendente=false,exports.state.transporte=null}function requireConfig(){if(!exports.state.config)throw new Error("[PushMesh] init() ainda não foi chamado. Como corrigir: chame PushMesh.init({ appId }) no boot do app.");return exports.state.config}function baseUrl(){return(requireConfig().baseUrl??exports.BASE_URL_PADRAO).replace(/\/+$/,"")}function emDesenvolvimento(){return"undefined"!=typeof __DEV__&&true===__DEV__}function validarBaseUrl(url){const limpa=url.trim();if(!(/^https:\/\//i.test(limpa)||/^http:\/\/(localhost|127\.0\.0\.1|10\.0\.2\.2|10\.0\.3\.2|\[::1\])(:\d+)?(\/|$)/i.test(limpa)&&emDesenvolvimento()))throw new Error("[PushMesh] baseUrl precisa ser https:// — pelo corpo das requisições trafegam o token de push e o external_user_id do usuário. Como corrigir: use https no servidor (http só é aceito para localhost em builds de desenvolvimento).")}Object.defineProperty(exports,"__esModule",{value:true}),exports.state=exports.BASE_URL_PADRAO=void 0,exports.__resetForTests=__resetForTests,exports.requireConfig=requireConfig,exports.baseUrl=baseUrl,exports.emDesenvolvimento=emDesenvolvimento,exports.validarBaseUrl=validarBaseUrl,exports.BASE_URL_PADRAO="https://api.pushmesh.io",exports.state={config:null,perfil:{},playerId:null,token:null,externalUserId:null,appStateSubscription:null,emBackgroundDesde:null,flushing:false,initEmVoo:null,logoutPendente:false,transporte:null};
|
package/dist/cjs/types.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export interface PushMeshConfig {
|
|
|
24
24
|
appId: string;
|
|
25
25
|
/**
|
|
26
26
|
* Base URL do servidor PUSHMESH. OPCIONAL — sem ela vale o servidor
|
|
27
|
-
* oficial (https://
|
|
27
|
+
* oficial (https://api.pushmesh.io); só o `appId` basta para
|
|
28
28
|
* integrar. Passe uma URL para servidor próprio ou local no desenvolvimento
|
|
29
29
|
* (http:// só é aceito para localhost em build de desenvolvimento).
|
|
30
30
|
*/
|
|
@@ -69,6 +69,22 @@ export interface PushMeshConfig {
|
|
|
69
69
|
* `<PushMeshInAppHost />` na raiz: é uma linha e é o caminho provado.
|
|
70
70
|
*/
|
|
71
71
|
inAppAutoMontar?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Pede a permissão de notificação logo no `init` — o atalho de UMA LINHA
|
|
74
|
+
* para quem quer o caminho mais curto até o push funcionando.
|
|
75
|
+
*
|
|
76
|
+
* **Continua sendo opt-in, e o padrão continua sendo NÃO pedir.** Quem
|
|
77
|
+
* escolhe o momento do pedido é o aplicativo, não a biblioteca: no iOS a
|
|
78
|
+
* pessoa só é perguntada UMA vez na vida do app, e queimar esse tiro no
|
|
79
|
+
* primeiro segundo — antes de qualquer contexto — é a forma clássica de
|
|
80
|
+
* perder metade da base para sempre.
|
|
81
|
+
*
|
|
82
|
+
* Ligue quando o seu app não tem um "momento certo" melhor (ferramenta
|
|
83
|
+
* interna, app cujo valor central É a notificação, protótipo). Para o resto,
|
|
84
|
+
* deixe desligado e chame `PushMesh.permissions.reativarNotificacoes()` no
|
|
85
|
+
* ponto do seu fluxo em que a permissão faz sentido para a pessoa.
|
|
86
|
+
*/
|
|
87
|
+
pedirPermissao?: boolean;
|
|
72
88
|
}
|
|
73
89
|
/**
|
|
74
90
|
* Ação (discriminada por `tipo`) — contrato In-App V1 §2.1 / V2 §2.5.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Images in iOS pushes — Notification Service Extension
|
|
2
|
+
|
|
3
|
+
**Time: ~5 minutes, once per app.** After this, every message carrying an image
|
|
4
|
+
shows that image on the iPhone, with nothing else to configure.
|
|
5
|
+
|
|
6
|
+
> 🇧🇷 **Este guia também existe em português:** [LEIA-ME.md](./LEIA-ME.md)
|
|
7
|
+
|
|
8
|
+
## Why there is a manual step here
|
|
9
|
+
|
|
10
|
+
This SDK is "your App ID is enough" — and it stays that way everywhere else.
|
|
11
|
+
This is the one step **Apple imposes** and no SDK can remove:
|
|
12
|
+
|
|
13
|
+
- the server already sends everything (`mutable-content: 1` and the image URL in
|
|
14
|
+
the payload). That part is done and does not depend on you;
|
|
15
|
+
- on iOS, whatever **downloads** the image is a separate process from your app,
|
|
16
|
+
called a *Notification Service Extension*. That process **only exists if your
|
|
17
|
+
project has a target of that type**;
|
|
18
|
+
- without that target, the iPhone ignores `mutable-content` **silently**: the
|
|
19
|
+
notification arrives perfectly, just with no image. This is exactly the
|
|
20
|
+
"the image shows on Android but not on iOS" symptom.
|
|
21
|
+
|
|
22
|
+
No code inside the app fixes it: with the app backgrounded or closed, the app
|
|
23
|
+
does not even run when the push arrives. On Android none of this is needed —
|
|
24
|
+
there the SDK itself downloads and displays.
|
|
25
|
+
|
|
26
|
+
## Step by step
|
|
27
|
+
|
|
28
|
+
1. Open your project in Xcode (`ios/YourApp.xcworkspace`).
|
|
29
|
+
2. **File → New → Target… → Notification Service Extension → Next**.
|
|
30
|
+
3. **Product Name**: `PushMeshNotificationService` (any name works).
|
|
31
|
+
Check **Team** (same as the app) and **Language: Swift** → **Finish**.
|
|
32
|
+
4. Xcode asks whether to activate the new *scheme*: click **Cancel** — that keeps
|
|
33
|
+
your app's scheme selected to run.
|
|
34
|
+
5. Xcode created a folder with a sample `NotificationService.swift`. Replace its
|
|
35
|
+
contents with ours, using **one command** at the root of your app:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
cp node_modules/@pushmesh/sdk/ios/NotificationService/PushMeshNotificationService.swift \
|
|
39
|
+
ios/PushMeshNotificationService/NotificationService.swift
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
(adjust the destination path if you named the target differently)
|
|
43
|
+
6. Run the app on a **real device**. Done.
|
|
44
|
+
|
|
45
|
+
No `pod install`, no Podfile changes and no `AppDelegate` edits: the extension
|
|
46
|
+
has no dependencies — only `Foundation` and `UserNotifications`, which ship with
|
|
47
|
+
the system.
|
|
48
|
+
|
|
49
|
+
## Details that cost you an afternoon if missed
|
|
50
|
+
|
|
51
|
+
| Point | What to do |
|
|
52
|
+
|---|---|
|
|
53
|
+
| **Minimum Deployments** | Set the extension target to the **same** value as the app (e.g. iOS 15.1). Xcode usually creates the target on the newest iOS — and then, on every device running an earlier version, the extension simply **does not run** (no error, no warning: the image just never appears). |
|
|
54
|
+
| **Bundle Identifier** | It must start with the app's: `com.yourcompany.app` → `com.yourcompany.app.PushMeshNotificationService`. Xcode gets this right on creation; but if you **rename the app's bundle later**, the extension's is **not** renamed with it and the build starts failing on signing. |
|
|
55
|
+
| **Team / signing** | Same Team as the app. With **automatic** signing, Xcode creates the extension's profile by itself. With **manual** signing, you need a **second App ID + provisioning profile** (for the extension's bundle) in Apple's portal — the classic oversight when signing by hand. |
|
|
56
|
+
| **NSExtensionPrincipalClass** | **Leave it alone.** The generated Info.plist points at `$(PRODUCT_MODULE_NAME).NotificationService`, and our file's class is called `NotificationService` precisely to match. Renaming the class without renaming it there makes the extension not run **silently**. |
|
|
57
|
+
| **If you prefer dragging the file** (instead of `cp`) | Delete the generated `NotificationService.swift` first — two classes with the same name will not compile. When dragging, **uncheck "Copy items if needed"** and set *Target Membership* to **the extension target only**, never the app. Upside: `npm update` refreshes the file for you. Downside: your build now depends on `node_modules`. |
|
|
58
|
+
|
|
59
|
+
### What is **not** needed (do not waste time)
|
|
60
|
+
|
|
61
|
+
- **App Groups**: no. Those would only matter for the extension to exchange data
|
|
62
|
+
with the app — it does not; it downloads the image and delivers.
|
|
63
|
+
- **Keychain Sharing**, **Background Modes** or **Push Notifications** *on the
|
|
64
|
+
extension target*: no. The **Push Notifications** capability stays on the
|
|
65
|
+
**app** target, where it already was.
|
|
66
|
+
- **CocoaPods / Podfile**: no. The extension has no dependencies at all.
|
|
67
|
+
- Touching the dashboard: no. Just fill in the image on the message — the server
|
|
68
|
+
already sends the flag that wakes the extension when there is media.
|
|
69
|
+
|
|
70
|
+
## Limits that are Apple's (and what the extension does with them)
|
|
71
|
+
|
|
72
|
+
- **Accepted formats**: `jpg`, `png`, `gif`. `webp`, `heic` and `avif` are
|
|
73
|
+
refused by iOS — serve jpg/png/gif from your CDN.
|
|
74
|
+
- **Size**: up to 10 MB. In practice a good push image is **under 1 MB** (the
|
|
75
|
+
device downloads it at display time, often on a poor network).
|
|
76
|
+
- **HTTPS**: mandatory. An `http://` URL is blocked by App Transport Security and
|
|
77
|
+
the download never leaves.
|
|
78
|
+
- **Deadline**: Apple gives the extension ~30s. Ours uses 10s per attempt and 20s
|
|
79
|
+
overall; if it runs out, it delivers the notification **without** the image.
|
|
80
|
+
- **How it looks**: thumbnail on the right of the banner; the large image appears
|
|
81
|
+
when the notification is **expanded** (pull down / long press).
|
|
82
|
+
|
|
83
|
+
On any failure — 404, refused format, CDN down, deadline exceeded — **the
|
|
84
|
+
notification still arrives complete, just without the image**. That is a law of
|
|
85
|
+
this file and it is proven by an executable test in the SDK suite
|
|
86
|
+
(`__tests__/ios-nse.test.ts`).
|
|
87
|
+
|
|
88
|
+
## How to tell it is working
|
|
89
|
+
|
|
90
|
+
With the iPhone connected to your Mac, open **Console.app**, select the device
|
|
91
|
+
and filter for `[PushMesh NSE]`. Every give-up is logged with the **cause** and
|
|
92
|
+
the **fix** (invalid URL, 404, unaccepted format, file too large, deadline
|
|
93
|
+
exceeded).
|
|
94
|
+
|
|
95
|
+
If **no** `[PushMesh NSE]` line shows up when a push with an image arrives, the
|
|
96
|
+
extension is not running — recheck *Minimum Deployments* and *Bundle Identifier*
|
|
97
|
+
in the table above.
|
|
98
|
+
|
|
99
|
+
> Test on a **real device**. On the simulator, notification extension behaviour
|
|
100
|
+
> varies with the Xcode version: a push without an image there proves nothing.
|
|
101
|
+
|
|
102
|
+
## What this extension does not do yet
|
|
103
|
+
|
|
104
|
+
**Delivery receipts in background / with the app closed.** Today iOS only counts
|
|
105
|
+
what the app sees (app open, and taps on the notification); on Android the
|
|
106
|
+
receipt is already real in all three states. That is the next front for this same
|
|
107
|
+
extension — when it lands, the installation step stays exactly this one, with no
|
|
108
|
+
rework.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pushmesh/sdk",
|
|
3
|
-
"version": "0.7.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.7.3-rc.3",
|
|
4
|
+
"description": "React Native SDK for PushMesh — push notifications with real delivery receipts, permission re-activation and in-app banners. Classic API included. (Guia em português: README.pt-BR.md)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
7
7
|
"main": "./dist/cjs/index.js",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"./package.json": "./package.json"
|
|
21
21
|
},
|
|
22
22
|
"bin": {
|
|
23
|
-
"pushmesh-doctor": "
|
|
23
|
+
"pushmesh-doctor": "dist/cjs/doctor.js"
|
|
24
24
|
},
|
|
25
25
|
"sideEffects": false,
|
|
26
26
|
"files": [
|
|
@@ -30,7 +30,10 @@
|
|
|
30
30
|
"pushmesh-sdk.podspec",
|
|
31
31
|
"react-native.config.js",
|
|
32
32
|
"README.md",
|
|
33
|
-
"LICENSE"
|
|
33
|
+
"LICENSE",
|
|
34
|
+
"README.pt-BR.md",
|
|
35
|
+
"app.plugin.js",
|
|
36
|
+
"plugin"
|
|
34
37
|
],
|
|
35
38
|
"homepage": "https://pushmesh.io",
|
|
36
39
|
"bugs": {
|
|
@@ -60,6 +63,7 @@
|
|
|
60
63
|
"react-native": ">=0.71.0"
|
|
61
64
|
},
|
|
62
65
|
"devDependencies": {
|
|
66
|
+
"@expo/config-plugins": "^54.0.0",
|
|
63
67
|
"@notifee/react-native": "^9.1.8",
|
|
64
68
|
"@react-native-async-storage/async-storage": "^2.1.2",
|
|
65
69
|
"@types/jest": "^29.5.14",
|
|
@@ -108,6 +112,15 @@
|
|
|
108
112
|
"delivery-receipt",
|
|
109
113
|
"android",
|
|
110
114
|
"ios",
|
|
111
|
-
"sdk"
|
|
115
|
+
"sdk",
|
|
116
|
+
"delivery-receipts",
|
|
117
|
+
"push-sdk",
|
|
118
|
+
"react-native-push",
|
|
119
|
+
"mobile-push",
|
|
120
|
+
"onesignal-alternative",
|
|
121
|
+
"in-app-banners",
|
|
122
|
+
"expo",
|
|
123
|
+
"expo-config-plugin",
|
|
124
|
+
"expo-plugin"
|
|
112
125
|
]
|
|
113
126
|
}
|
package/plugin/index.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin de configuração do Expo para o @pushmesh/sdk.
|
|
3
|
+
*
|
|
4
|
+
* POR QUE EXISTE: num app Expo o `ios/` e o `android/` são GERADOS no prebuild,
|
|
5
|
+
* então tudo que se edita à mão no Xcode é apagado na próxima geração. Sem
|
|
6
|
+
* plugin, integrar push num app Expo obriga a sair do fluxo gerenciado — que é
|
|
7
|
+
* exatamente o oposto da promessa "uma linha e funciona".
|
|
8
|
+
*
|
|
9
|
+
* O QUE ELE FAZ (e o que deliberadamente NÃO faz):
|
|
10
|
+
*
|
|
11
|
+
* Android → NADA. E isso é a vantagem, não uma lacuna: o módulo nativo já
|
|
12
|
+
* declara POST_NOTIFICATIONS, o receiver e o trampolim de clique no próprio
|
|
13
|
+
* AndroidManifest, e o modo zero-config busca os parâmetros do Firebase em
|
|
14
|
+
* tempo de execução (GET /api/v1/apps/{id}/firebase_params). O app NÃO
|
|
15
|
+
* precisa do google-services.json nem do plugin com.google.gms — o que
|
|
16
|
+
* outros SDKs de push exigem e o que, num app Expo, significa carregar um
|
|
17
|
+
* arquivo de credencial no repositório.
|
|
18
|
+
*
|
|
19
|
+
* iOS → o que a Apple obriga e nenhum SDK consegue evitar:
|
|
20
|
+
* 1. a capability Push Notifications (entitlement `aps-environment`);
|
|
21
|
+
* 2. `remote-notification` em UIBackgroundModes — sem isso o iOS não
|
|
22
|
+
* entrega push com o app em segundo plano;
|
|
23
|
+
* 3. a Notification Service Extension, que é a ÚNICA forma de o iPhone
|
|
24
|
+
* baixar a imagem de um push. É o passo que a documentação descreve
|
|
25
|
+
* como "5 minutos no Xcode, uma vez por app": aqui ele vira zero
|
|
26
|
+
* minutos, e sobrevive ao prebuild.
|
|
27
|
+
*
|
|
28
|
+
* USO no app.json / app.config.js:
|
|
29
|
+
*
|
|
30
|
+
* { "expo": { "plugins": [
|
|
31
|
+
* ["@pushmesh/sdk", { "modo": "producao" }]
|
|
32
|
+
* ] } }
|
|
33
|
+
*
|
|
34
|
+
* Sem opções, os padrões cobrem o caso comum.
|
|
35
|
+
*/
|
|
36
|
+
const {
|
|
37
|
+
AndroidConfig,
|
|
38
|
+
withEntitlementsPlist,
|
|
39
|
+
withInfoPlist,
|
|
40
|
+
withXcodeProject,
|
|
41
|
+
withDangerousMod,
|
|
42
|
+
createRunOncePlugin,
|
|
43
|
+
} = require('@expo/config-plugins');
|
|
44
|
+
const fs = require('node:fs');
|
|
45
|
+
const path = require('node:path');
|
|
46
|
+
|
|
47
|
+
const pkg = require('../package.json');
|
|
48
|
+
|
|
49
|
+
/** Nome do alvo da extensão. Fixo de propósito: o Info.plist gerado aponta
|
|
50
|
+
* para `$(PRODUCT_MODULE_NAME).NotificationService`, e renomear sem renomear
|
|
51
|
+
* lá faz a extensão não rodar EM SILÊNCIO. */
|
|
52
|
+
const ALVO_NSE = 'PushMeshNotificationService';
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// iOS — 1. capability Push Notifications
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
const comEntitlementPush = (config, { modo }) =>
|
|
58
|
+
withEntitlementsPlist(config, cfg => {
|
|
59
|
+
// 'development' usa o APNs de sandbox; 'production' o de produção. Errar
|
|
60
|
+
// isto é a armadilha clássica do push no iOS: o token de um ambiente é
|
|
61
|
+
// recusado no outro com BadDeviceToken, e o sintoma é "não chega nada".
|
|
62
|
+
// TestFlight e App Store usam PRODUÇÃO, mesmo em build de teste.
|
|
63
|
+
cfg.modResults['aps-environment'] = modo === 'producao' ? 'production' : 'development';
|
|
64
|
+
return cfg;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// iOS — 2. entrega em segundo plano
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
const comBackgroundModes = config =>
|
|
71
|
+
withInfoPlist(config, cfg => {
|
|
72
|
+
const modos = cfg.modResults.UIBackgroundModes ?? [];
|
|
73
|
+
if (!modos.includes('remote-notification')) modos.push('remote-notification');
|
|
74
|
+
cfg.modResults.UIBackgroundModes = modos;
|
|
75
|
+
return cfg;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// iOS — 3. a Notification Service Extension (imagem no push)
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
/** Escreve os arquivos da extensão dentro de `ios/<ALVO_NSE>/`. */
|
|
83
|
+
const comArquivosDaExtensao = config =>
|
|
84
|
+
withDangerousMod(config, [
|
|
85
|
+
'ios',
|
|
86
|
+
async cfg => {
|
|
87
|
+
const destino = path.join(cfg.modRequest.platformProjectRoot, ALVO_NSE);
|
|
88
|
+
fs.mkdirSync(destino, { recursive: true });
|
|
89
|
+
|
|
90
|
+
// O Swift vem do pacote: uma fonte só para o código da extensão, então
|
|
91
|
+
// `npm update` atualiza a extensão junto com o SDK.
|
|
92
|
+
const origem = path.join(
|
|
93
|
+
__dirname,
|
|
94
|
+
'..',
|
|
95
|
+
'ios',
|
|
96
|
+
'NotificationService',
|
|
97
|
+
'PushMeshNotificationService.swift',
|
|
98
|
+
);
|
|
99
|
+
fs.copyFileSync(origem, path.join(destino, 'NotificationService.swift'));
|
|
100
|
+
|
|
101
|
+
// NSExtensionPrincipalClass casa com a classe do arquivo acima.
|
|
102
|
+
fs.writeFileSync(
|
|
103
|
+
path.join(destino, 'Info.plist'),
|
|
104
|
+
`<?xml version="1.0" encoding="UTF-8"?>
|
|
105
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
106
|
+
<plist version="1.0">
|
|
107
|
+
<dict>
|
|
108
|
+
<key>CFBundleDisplayName</key><string>${ALVO_NSE}</string>
|
|
109
|
+
<key>CFBundleExecutable</key><string>$(EXECUTABLE_NAME)</string>
|
|
110
|
+
<key>CFBundleIdentifier</key><string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
|
111
|
+
<key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
|
|
112
|
+
<key>CFBundleName</key><string>$(PRODUCT_NAME)</string>
|
|
113
|
+
<key>CFBundlePackageType</key><string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
|
114
|
+
<key>CFBundleShortVersionString</key><string>$(MARKETING_VERSION)</string>
|
|
115
|
+
<key>CFBundleVersion</key><string>$(CURRENT_PROJECT_VERSION)</string>
|
|
116
|
+
<key>NSExtension</key>
|
|
117
|
+
<dict>
|
|
118
|
+
<key>NSExtensionPointIdentifier</key><string>com.apple.usernotifications.service</string>
|
|
119
|
+
<key>NSExtensionPrincipalClass</key><string>$(PRODUCT_MODULE_NAME).NotificationService</string>
|
|
120
|
+
</dict>
|
|
121
|
+
</dict>
|
|
122
|
+
</plist>
|
|
123
|
+
`,
|
|
124
|
+
);
|
|
125
|
+
return cfg;
|
|
126
|
+
},
|
|
127
|
+
]);
|
|
128
|
+
|
|
129
|
+
/** Cria o alvo da extensão no projeto do Xcode. */
|
|
130
|
+
const comAlvoDaExtensao = config =>
|
|
131
|
+
withXcodeProject(config, cfg => {
|
|
132
|
+
const projeto = cfg.modResults;
|
|
133
|
+
|
|
134
|
+
// Idempotente: o prebuild roda de novo a cada `expo prebuild`, e criar o
|
|
135
|
+
// alvo duas vezes deixa o projeto impossível de compilar.
|
|
136
|
+
if (projeto.pbxTargetByName(ALVO_NSE)) return cfg;
|
|
137
|
+
|
|
138
|
+
const bundleApp = cfg.ios?.bundleIdentifier;
|
|
139
|
+
if (!bundleApp) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
'[@pushmesh/sdk] defina ios.bundleIdentifier no app.json antes do prebuild — ' +
|
|
142
|
+
'o bundle da extensão deriva dele.',
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const grupo = projeto.addPbxGroup(
|
|
147
|
+
['NotificationService.swift', 'Info.plist'],
|
|
148
|
+
ALVO_NSE,
|
|
149
|
+
ALVO_NSE,
|
|
150
|
+
);
|
|
151
|
+
// Pendura o grupo na raiz do projeto (o grupo sem nome é a raiz).
|
|
152
|
+
const grupos = projeto.hash.project.objects.PBXGroup;
|
|
153
|
+
for (const chave of Object.keys(grupos)) {
|
|
154
|
+
if (grupos[chave].name === undefined && grupos[chave].path === undefined) {
|
|
155
|
+
projeto.addToPbxGroup(grupo.uuid, chave);
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const alvo = projeto.addTarget(ALVO_NSE, 'app_extension', ALVO_NSE, `${bundleApp}.${ALVO_NSE}`);
|
|
161
|
+
projeto.addBuildPhase([], 'PBXSourcesBuildPhase', 'Sources', alvo.uuid);
|
|
162
|
+
projeto.addBuildPhase([], 'PBXResourcesBuildPhase', 'Resources', alvo.uuid);
|
|
163
|
+
projeto.addBuildPhase([], 'PBXFrameworksBuildPhase', 'Frameworks', alvo.uuid);
|
|
164
|
+
projeto.addSourceFile('NotificationService.swift', { target: alvo.uuid }, grupo.uuid);
|
|
165
|
+
|
|
166
|
+
// O alvo TEM de aceitar as mesmas versões de iOS que o app: o Xcode cria a
|
|
167
|
+
// extensão no iOS mais novo, e aí ela simplesmente NÃO RODA em aparelho
|
|
168
|
+
// com versão anterior — sem erro, sem aviso, a imagem só não aparece.
|
|
169
|
+
const minimoDoApp = cfg.ios?.deploymentTarget ?? '15.1';
|
|
170
|
+
const configs = projeto.pbxXCBuildConfigurationSection();
|
|
171
|
+
for (const chave of Object.keys(configs)) {
|
|
172
|
+
const build = configs[chave].buildSettings;
|
|
173
|
+
if (!build || build.PRODUCT_NAME !== `"${ALVO_NSE}"`) continue;
|
|
174
|
+
build.IPHONEOS_DEPLOYMENT_TARGET = minimoDoApp;
|
|
175
|
+
build.INFOPLIST_FILE = `"${ALVO_NSE}/Info.plist"`;
|
|
176
|
+
build.SWIFT_VERSION = '5.0';
|
|
177
|
+
build.TARGETED_DEVICE_FAMILY = '"1,2"';
|
|
178
|
+
build.CODE_SIGN_STYLE = 'Automatic';
|
|
179
|
+
}
|
|
180
|
+
return cfg;
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// Android — só o ícone/cor, quando o app pedir
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
const comIconeAndroid = (config, { iconeNotificacao, corNotificacao }) => {
|
|
187
|
+
if (!iconeNotificacao && !corNotificacao) return config;
|
|
188
|
+
return AndroidConfig.Manifest.withAndroidManifest(config, cfg => {
|
|
189
|
+
const app = AndroidConfig.Manifest.getMainApplicationOrThrow(cfg.modResults);
|
|
190
|
+
const meta = (nome, valor) =>
|
|
191
|
+
AndroidConfig.Manifest.addMetaDataItemToMainApplication(app, nome, valor, 'resource');
|
|
192
|
+
if (iconeNotificacao) meta('com.google.firebase.messaging.default_notification_icon', iconeNotificacao);
|
|
193
|
+
if (corNotificacao) meta('com.google.firebase.messaging.default_notification_color', corNotificacao);
|
|
194
|
+
return cfg;
|
|
195
|
+
});
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
const comPushMesh = (config, opcoes = {}) => {
|
|
200
|
+
const {
|
|
201
|
+
modo = 'desenvolvimento',
|
|
202
|
+
imagemNoPush = true,
|
|
203
|
+
iconeNotificacao,
|
|
204
|
+
corNotificacao,
|
|
205
|
+
} = opcoes;
|
|
206
|
+
|
|
207
|
+
if (modo !== 'desenvolvimento' && modo !== 'producao') {
|
|
208
|
+
throw new Error(
|
|
209
|
+
`[@pushmesh/sdk] modo "${modo}" desconhecido — use "desenvolvimento" ou "producao".`,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let cfg = config;
|
|
214
|
+
cfg = comEntitlementPush(cfg, { modo });
|
|
215
|
+
cfg = comBackgroundModes(cfg);
|
|
216
|
+
if (imagemNoPush) {
|
|
217
|
+
cfg = comArquivosDaExtensao(cfg);
|
|
218
|
+
cfg = comAlvoDaExtensao(cfg);
|
|
219
|
+
}
|
|
220
|
+
cfg = comIconeAndroid(cfg, { iconeNotificacao, corNotificacao });
|
|
221
|
+
return cfg;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// `createRunOncePlugin` evita aplicar duas vezes quando o plugin aparece em
|
|
225
|
+
// mais de um lugar da árvore de configuração.
|
|
226
|
+
module.exports = createRunOncePlugin(comPushMesh, pkg.name, pkg.version);
|