@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 CHANGED
@@ -1,64 +1,95 @@
1
1
  # @pushmesh/sdk
2
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.
3
+ React Native SDK for **PushMesh** — push notifications with **real delivery
4
+ receipts**, permission re-activation and in-app banners.
5
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
6
+ > 🇧🇷 **Este guia também existe em português:** [README.pt-BR.md](./README.pt-BR.md)
14
7
 
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).
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
- ## Instalação
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
- ## Começo rápido (zero-config recomendado)
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
- 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:
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: 'UUID_DO_APP_NO_PUSHMESH', // ISSO é obrigatório
67
+ appId: 'YOUR_APP_UUID_IN_PUSHMESH', // the ONLY required field
39
68
  appVersion: '1.0.3',
40
- // baseUrl é OPCIONALsem ela vale o servidor oficial
41
- // (https://push.pushmesh.io). Passe uma URL para self-host.
69
+ // baseUrl is OPTIONALwithout it the official server is used
70
+ // (https://api.pushmesh.io). Pass a URL only when self-hosting.
42
71
  });
43
- // no login do app: PushMesh.login(idDoUsuario);
72
+ // when your app logs the user in: PushMesh.login(userId);
44
73
  ```
45
74
 
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.
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
- 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`.
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
- ## Modo host (app com Firebase próprio)
85
+ ## Host mode (app with its own Firebase)
56
86
 
57
- Quem tem Firebase passa `getToken` e nada muda (o zero-config nem é acionado):
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: 'UUID_DO_APP_NO_PUSHMESH',
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` (gatilho no aplicativo) ·
107
+ `definirGatilho`/`removerGatilho`/`limparGatilhos` (in-app triggers) ·
77
108
  `triggers.*` · `doctor()`.
78
109
 
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`.
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
- ## Leve por contrato
115
+ ## Light by contract
84
116
 
85
- O SDK roda no celular do cliente finalcada request e cada kB são orçados:
117
+ This SDK runs on your end users' phonesevery request and every kB is
118
+ budgeted:
86
119
 
87
- - **Zero polling** — rede 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).
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
- ## Diagnóstico
130
+ ## Diagnostics
96
131
 
97
132
  ```bash
98
- npx pushmesh-doctor --base-url https://push.pushmesh.io \
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` = 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).
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
- ## Requisitos
145
+ ## Requirements
107
146
 
108
- | Item | Mínimo |
147
+ | Item | Minimum |
109
148
  |---|---|
110
- | React Native | 0.71 (New Architecture testada em 0.83) |
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 (alinhado ao mínimo do RN do app) |
114
- | Android | conforme o `minSdk` do app |
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 e
155
+ Peer dependencies: `@notifee/react-native` ≥ 7 and
117
156
  `@react-native-async-storage/async-storage` ≥ 1.17.
118
157
 
119
- ## Desenvolvimento do SDK
158
+ ## Developing the SDK
120
159
 
121
160
  ```bash
122
161
  npm install
123
162
  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
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
- ```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
- ```
169
+ ## License
152
170
 
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.
171
+ MIT PushMesh 2026. See [LICENSE](LICENSE).
@@ -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))}
@@ -7,7 +7,7 @@
7
7
  * (a lei do erro que ensina).
8
8
  *
9
9
  * Uso:
10
- * npx pushmesh-doctor --base-url https://push.pushmesh.io \
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://push.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";
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>;
@@ -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://push.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://push.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)});
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)});
@@ -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://push.pushmesh.io).
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;
@@ -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.2";
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;
@@ -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.2";
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;
@@ -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}`)}
@@ -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://push.pushmesh.io";
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://push.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};
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};
@@ -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://push.pushmesh.io); só o `appId` basta para
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.2",
4
- "description": "SDK React Native do PUSHMESH — push com recibo real de entrega, reativação de permissão e banners in-app. API clássica inclusa.",
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": "./dist/cjs/doctor.js"
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
  }
@@ -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);