@pushmesh/sdk 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +160 -0
- package/android/build.gradle +82 -0
- package/android/src/main/AndroidManifest.xml +69 -0
- package/android/src/main/java/io/pushmesh/sdk/PushMeshClickActivity.kt +124 -0
- package/android/src/main/java/io/pushmesh/sdk/PushMeshFirebaseModule.kt +512 -0
- package/android/src/main/java/io/pushmesh/sdk/PushMeshFirebasePackage.kt +20 -0
- package/android/src/main/java/io/pushmesh/sdk/PushMeshMessageBuffer.kt +53 -0
- package/android/src/main/java/io/pushmesh/sdk/PushMeshMessageRouter.kt +477 -0
- package/android/src/main/java/io/pushmesh/sdk/PushMeshMessagingReceiver.kt +77 -0
- package/android/src/main/java/io/pushmesh/sdk/PushMeshMessagingService.kt +88 -0
- package/dist/cjs/boot.d.ts +7 -0
- package/dist/cjs/boot.js +1 -0
- package/dist/cjs/compat.d.ts +63 -0
- package/dist/cjs/compat.js +1 -0
- package/dist/cjs/doctor.d.ts +27 -0
- package/dist/cjs/doctor.js +2 -0
- package/dist/cjs/events.d.ts +21 -0
- package/dist/cjs/events.js +1 -0
- package/dist/cjs/http.d.ts +20 -0
- package/dist/cjs/http.js +1 -0
- package/dist/cjs/inapp/SkeletonShimmer.d.ts +1 -0
- package/dist/cjs/inapp/SkeletonShimmer.js +1 -0
- package/dist/cjs/inapp/auto.d.ts +1 -0
- package/dist/cjs/inapp/auto.js +1 -0
- package/dist/cjs/inapp/components.d.ts +14 -0
- package/dist/cjs/inapp/components.js +1 -0
- package/dist/cjs/inapp/engine.d.ts +1 -0
- package/dist/cjs/inapp/engine.js +1 -0
- package/dist/cjs/inapp/imageCache.d.ts +1 -0
- package/dist/cjs/inapp/imageCache.js +1 -0
- package/dist/cjs/inapp/index.d.ts +20 -0
- package/dist/cjs/inapp/index.js +1 -0
- package/dist/cjs/inapp/lifecycle.d.ts +1 -0
- package/dist/cjs/inapp/lifecycle.js +1 -0
- package/dist/cjs/inapp/normalize.d.ts +1 -0
- package/dist/cjs/inapp/normalize.js +1 -0
- package/dist/cjs/inapp/registro.d.ts +1 -0
- package/dist/cjs/inapp/registro.js +1 -0
- package/dist/cjs/inapp/sessao.d.ts +1 -0
- package/dist/cjs/inapp/sessao.js +1 -0
- package/dist/cjs/index.d.ts +118 -0
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/perfil.d.ts +52 -0
- package/dist/cjs/perfil.js +1 -0
- package/dist/cjs/permissions.d.ts +54 -0
- package/dist/cjs/permissions.js +1 -0
- package/dist/cjs/players.d.ts +20 -0
- package/dist/cjs/players.js +1 -0
- package/dist/cjs/queue.d.ts +1 -0
- package/dist/cjs/queue.js +1 -0
- package/dist/cjs/receipts.d.ts +32 -0
- package/dist/cjs/receipts.js +1 -0
- package/dist/cjs/state.d.ts +21 -0
- package/dist/cjs/state.js +1 -0
- package/dist/cjs/storage.d.ts +1 -0
- package/dist/cjs/storage.js +1 -0
- package/dist/cjs/tags.d.ts +13 -0
- package/dist/cjs/tags.js +1 -0
- package/dist/cjs/triggers.d.ts +22 -0
- package/dist/cjs/triggers.js +1 -0
- package/dist/cjs/types.d.ts +258 -0
- package/dist/cjs/types.js +1 -0
- package/dist/cjs/zeroconfig.d.ts +45 -0
- package/dist/cjs/zeroconfig.js +1 -0
- package/ios/NotificationService/LEIA-ME.md +105 -0
- package/ios/NotificationService/PushMeshNotificationService.swift +384 -0
- package/ios/PushMeshApns.swift +746 -0
- package/ios/PushMeshApnsModule.m +76 -0
- package/ios/PushMeshSwizzleRegistro.swift +141 -0
- package/package.json +113 -0
- package/pushmesh-sdk.podspec +32 -0
- package/react-native.config.js +17 -0
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
import CryptoKit
|
|
2
|
+
import Foundation
|
|
3
|
+
import Network
|
|
4
|
+
import React
|
|
5
|
+
import UIKit
|
|
6
|
+
import UserNotifications
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Módulo nativo iOS do modo ZERO-CONFIG: APNs DIRETO — sem Firebase e sem
|
|
10
|
+
* GoogleService-Info.plist no app do cliente (a chave .p8 fica no painel do
|
|
11
|
+
* servidor PUSHMESH).
|
|
12
|
+
*
|
|
13
|
+
* - getToken(): devolve o token APNs cru (64 hex). Se ainda não capturado,
|
|
14
|
+
* chama registerForRemoteNotifications() e resolve quando o delegate chegar
|
|
15
|
+
* (timeout de 10s com erro que ensina).
|
|
16
|
+
* - O token é capturado via METHOD SWIZZLING dos callbacks de APNs do
|
|
17
|
+
* AppDelegate. Por quê: exigir código no AppDelegate do host quebraria a
|
|
18
|
+
* premissa zero-config. É a técnica padrão de mercado para SDKs
|
|
19
|
+
* "zero-código", e aqui é feita do jeito seguro: o IMP original (próprio ou
|
|
20
|
+
* herdado) é SEMPRE encadeado — outro SDK que também faça swizzling convive,
|
|
21
|
+
* porque cada camada chama a anterior. Quem lembra o original é o
|
|
22
|
+
* `PushMeshSwizzleRegistro`, POR CLASSE: leia as leis lá (troca de classe
|
|
23
|
+
* de delegate A→B→A e encadeamento entre SDKs já quebraram este arquivo).
|
|
24
|
+
* - Evento "PushMeshApnsToken" quando o token MUDA (o JS re-registra o
|
|
25
|
+
* player). Vai pelo RCTEventEmitter → chega ao JS via DeviceEventEmitter.
|
|
26
|
+
*
|
|
27
|
+
* A PERMISSÃO de notificação NÃO é pedida aqui — já é responsabilidade do
|
|
28
|
+
* notifee (src/permissions.ts). registerForRemoteNotifications() com a
|
|
29
|
+
* permissão negada simplesmente não entrega token (didFail/timeout).
|
|
30
|
+
*
|
|
31
|
+
* Módulo old-arch (RCTEventEmitter/NativeModule): funciona na New
|
|
32
|
+
* Architecture (Fabric) via interop layer do RN. TurboModule/codegen fica
|
|
33
|
+
* para uma fase posterior.
|
|
34
|
+
*/
|
|
35
|
+
@objc(PushMeshApns)
|
|
36
|
+
final class PushMeshApns: RCTEventEmitter {
|
|
37
|
+
|
|
38
|
+
// MARK: - Estado
|
|
39
|
+
|
|
40
|
+
private static weak var shared: PushMeshApns?
|
|
41
|
+
private var apnsToken: String?
|
|
42
|
+
private var hasListeners = false
|
|
43
|
+
private var pendentes: [(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock)] = []
|
|
44
|
+
|
|
45
|
+
private static let timeoutSegundos: TimeInterval = 10
|
|
46
|
+
|
|
47
|
+
override init() {
|
|
48
|
+
super.init()
|
|
49
|
+
PushMeshApns.shared = self
|
|
50
|
+
// O swizzling acontece na 1ª instanciação do módulo (boot do SDK). Em apps
|
|
51
|
+
// RN o AppDelegate já existe nesse momento; requiresMainQueueSetup = true
|
|
52
|
+
// garante que isto roda na main queue.
|
|
53
|
+
PushMeshApns.swizzleAppDelegate()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
override static func requiresMainQueueSetup() -> Bool { true }
|
|
57
|
+
|
|
58
|
+
override func supportedEvents() -> [String]! {
|
|
59
|
+
["PushMeshApnsToken", "PushMeshApnsMessage", "PushMeshApnsClick"]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
override func startObserving() {
|
|
63
|
+
hasListeners = true
|
|
64
|
+
// o JS assinou os eventos: garante o swizzling do delegate de
|
|
65
|
+
// notificações (o notifee/host pode tê-lo definido DEPOIS do nosso init)
|
|
66
|
+
DispatchQueue.main.async { PushMeshApns.swizzleNotificationDelegateSePreciso() }
|
|
67
|
+
}
|
|
68
|
+
override func stopObserving() { hasListeners = false }
|
|
69
|
+
|
|
70
|
+
// MARK: - API JS
|
|
71
|
+
|
|
72
|
+
/// Garante o registro no APNs (idempotente no sistema).
|
|
73
|
+
@objc func register() {
|
|
74
|
+
DispatchQueue.main.async {
|
|
75
|
+
PushMeshApns.swizzleNotificationDelegateSePreciso()
|
|
76
|
+
UIApplication.shared.registerForRemoteNotifications()
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Abre DIRETO a tela de Notificações do app nos Ajustes (iOS 16+).
|
|
82
|
+
*
|
|
83
|
+
* Por que existe: `Linking.openSettings()` abre a página GERAL do app — e
|
|
84
|
+
* quando o app nunca pediu permissão ele sequer TEM página lá, então o
|
|
85
|
+
* sistema abre a raiz dos Ajustes e a pessoa se perde. O
|
|
86
|
+
* `openNotificationSettingsURLString` é a API PÚBLICA da Apple
|
|
87
|
+
* para o atalho certo; `App-prefs:` seria privada e reprovaria na revisão.
|
|
88
|
+
*
|
|
89
|
+
* Resolve `false` quando não dá (iOS < 16 ou URL recusada) — aí o JS cai no
|
|
90
|
+
* caminho padrão. Nunca lança: o push jamais derruba o app do cliente.
|
|
91
|
+
*/
|
|
92
|
+
@objc func abrirAjustesDeNotificacao(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
93
|
+
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
94
|
+
DispatchQueue.main.async {
|
|
95
|
+
guard #available(iOS 16.0, *) else { resolve(false); return }
|
|
96
|
+
guard let url = URL(string: UIApplication.openNotificationSettingsURLString) else {
|
|
97
|
+
resolve(false); return
|
|
98
|
+
}
|
|
99
|
+
// sem canOpenURL de propósito (lição da casa: ele devolve false para
|
|
100
|
+
// esquema não declarado no Info.plist do host) — o próprio `open` diz se
|
|
101
|
+
// deu certo, e o JS cai no caminho padrão quando não dá.
|
|
102
|
+
UIApplication.shared.open(url, options: [:]) { ok in resolve(ok) }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Token APNs em hex (64 chars). SEMPRE pede um token fresco ao sistema
|
|
108
|
+
* (registerForRemoteNotifications dispara didRegister rápido quando já há
|
|
109
|
+
* registro) — rotação nunca fica escondida atrás de cache. No timeout,
|
|
110
|
+
* resolve o token pendente persistido (UserDefaults) se houver; só rejeita
|
|
111
|
+
* quando não há NADA (ex.: simulador sem capability).
|
|
112
|
+
*/
|
|
113
|
+
@objc func getToken(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
114
|
+
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
115
|
+
DispatchQueue.main.async {
|
|
116
|
+
if let token = self.apnsToken {
|
|
117
|
+
resolve(token) // capturado NESTE processo = fresco
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
self.pendentes.append((resolve, reject))
|
|
121
|
+
PushMeshApns.swizzleNotificationDelegateSePreciso()
|
|
122
|
+
UIApplication.shared.registerForRemoteNotifications()
|
|
123
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + PushMeshApns.timeoutSegundos) { [weak self] in
|
|
124
|
+
guard let self = self, !self.pendentes.isEmpty else { return }
|
|
125
|
+
if let pendente = UserDefaults.standard.string(forKey: PushMeshApns.chaveTokenPendente) {
|
|
126
|
+
let pend = self.pendentes
|
|
127
|
+
self.pendentes.removeAll()
|
|
128
|
+
pend.forEach { $0.resolve(pendente) }
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
self.rejeitarPendentes(
|
|
132
|
+
codigo: "pushmesh_apns_timeout",
|
|
133
|
+
mensagem: "[PushMesh] token APNs não chegou em 10s. Como corrigir: rode num device FÍSICO " +
|
|
134
|
+
"com a capability 'Push Notifications' habilitada no Xcode (e Background Modes → " +
|
|
135
|
+
"Remote notifications). Simuladores antigos não recebem token APNs; se o app host " +
|
|
136
|
+
"gerencia o APNs por conta própria, prefira o modo host (getToken no init).",
|
|
137
|
+
erro: nil
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Clique em notificação com o app MORTO (iOS): o didReceive chegou sem JS
|
|
145
|
+
* e o payload ficou guardado. Lê e LIMPA. null quando não houve clique.
|
|
146
|
+
*/
|
|
147
|
+
@objc func getPendingClick(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
148
|
+
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
149
|
+
let pendente = UserDefaults.standard.dictionary(forKey: PushMeshApns.chaveClickPendente)
|
|
150
|
+
UserDefaults.standard.removeObject(forKey: PushMeshApns.chaveClickPendente)
|
|
151
|
+
resolve(pendente ?? NSNull())
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* PERFIL DO APARELHO — nível GRATUITO de rastreamento (espelha o método
|
|
156
|
+
* homônimo do módulo Android; as chaves são as mesmas).
|
|
157
|
+
*
|
|
158
|
+
* Só o que NÃO pede permissão e NÃO altera a ficha de privacidade do app do
|
|
159
|
+
* cliente na App Store. IDFA e localização ficam de fora por decisão de
|
|
160
|
+
* produto: ambos alterariam a ficha de privacidade e exigiriam consentimento.
|
|
161
|
+
*
|
|
162
|
+
* `carrier` NÃO é reportado de propósito: a Apple deprecou `CTCarrier` na
|
|
163
|
+
* iOS 16 e ele devolve "--". Preferimos campo ausente a dado inventado.
|
|
164
|
+
*
|
|
165
|
+
* Nunca rejeita: perfil ausente não pode impedir o aparelho de registrar.
|
|
166
|
+
*/
|
|
167
|
+
@objc func perfilDispositivo(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
168
|
+
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
169
|
+
var perfil: [String: Any] = [
|
|
170
|
+
"device_os": UIDevice.current.systemVersion,
|
|
171
|
+
"fabricante": "Apple",
|
|
172
|
+
"modelo": PushMeshApns.modeloHardware(),
|
|
173
|
+
"rooted": PushMeshApns.indicioDeJailbreak(),
|
|
174
|
+
]
|
|
175
|
+
if let rede = PushMeshApns.tipoDeRede() {
|
|
176
|
+
perfil["net_type"] = rede
|
|
177
|
+
}
|
|
178
|
+
if let bits = PushMeshApns.bitmaskNotificacoes() {
|
|
179
|
+
perfil["notif_permissoes"] = bits
|
|
180
|
+
}
|
|
181
|
+
if let inst = PushMeshApns.idDaInstalacao() {
|
|
182
|
+
perfil["instalacao_id"] = inst
|
|
183
|
+
}
|
|
184
|
+
resolve(perfil)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Identificador estável do APARELHO — em HASH, nunca cru.
|
|
189
|
+
*
|
|
190
|
+
* Fonte: `identifierForVendor` (IDFV). Não pede permissão, não é o IDFA (não
|
|
191
|
+
* exige ATT) e é escopado ao fornecedor: não correlaciona entre empresas.
|
|
192
|
+
* Sobrevive à reinstalação enquanto houver ao menos um app do mesmo
|
|
193
|
+
* fornecedor no aparelho — é o que permite derrubar o token fantasma na
|
|
194
|
+
* hora, sem esperar um envio falhar.
|
|
195
|
+
*
|
|
196
|
+
* SHA-256 truncado: comparar igualdade basta, e o valor cru fica no aparelho.
|
|
197
|
+
*
|
|
198
|
+
* KEYCHAIN FOI CONSIDERADO E DESCARTADO (decisão de produto — não é um
|
|
199
|
+
* esquecimento a ser "melhorado" depois: trocar a fonte aqui muda o id de
|
|
200
|
+
* TODO o parque de aparelhos de uma vez). Um id gravado no Keychain
|
|
201
|
+
* sobreviveria à desinstalação de TODOS os apps do fornecedor, e é
|
|
202
|
+
* exatamente aí que ele deixa de ser "identidade da instalação" e vira
|
|
203
|
+
* RASTREAMENTO do aparelho além do consentimento — o que a política da Apple
|
|
204
|
+
* veta sem ATT/permissão explícita. O IDFV, que zera quando o último app do
|
|
205
|
+
* fornecedor sai, é o limite que aceitamos: some o aparelho, some o id.
|
|
206
|
+
* Consequência ASSUMIDA e medida: no iOS a reinstalação não derruba o token
|
|
207
|
+
* fantasma como derruba no Android.
|
|
208
|
+
*/
|
|
209
|
+
static func idDaInstalacao() -> String? {
|
|
210
|
+
guard let idfv = UIDevice.current.identifierForVendor?.uuidString else { return nil }
|
|
211
|
+
// POR QUE O SAL: não é anti-reversão — IDFV é UUID com ~122 bits
|
|
212
|
+
// aleatórios, força bruta não volta dele com ou sem sal. O sal fixo do SDK
|
|
213
|
+
// serve para DESCORRELACIONAR: outro SDK que hasheie o MESMO IDFV sem esse
|
|
214
|
+
// prefixo chega a outro dígito, então ninguém cruza o nosso id com o dele.
|
|
215
|
+
// Fixo (e não aleatório) de propósito: o id precisa ser reproduzível a
|
|
216
|
+
// cada boot, senão deixa de identificar a instalação.
|
|
217
|
+
let digest = SHA256.hash(data: Data("pushmesh:\(idfv)".utf8))
|
|
218
|
+
return digest.prefix(16).map { String(format: "%02x", $0) }.joined()
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* BITMASK granular da permissão (contrato da migration 0013 — mesmo do
|
|
223
|
+
* Android): 1=habilitadas · 2=alerta/banner · 4=som · 8=badge · 16=tela de
|
|
224
|
+
* bloqueio · 32=central de notificações. É o que diagnostica "tenho
|
|
225
|
+
* permissão mas não vejo nada" (alerta ok, banner desligado).
|
|
226
|
+
*
|
|
227
|
+
* getNotificationSettings é assíncrono; espera CURTA (500 ms) — sem
|
|
228
|
+
* resposta, o campo não vai (nunca atrasar o registro por diagnóstico).
|
|
229
|
+
*/
|
|
230
|
+
private static func bitmaskNotificacoes() -> Int? {
|
|
231
|
+
var bits: Int?
|
|
232
|
+
let semaforo = DispatchSemaphore(value: 0)
|
|
233
|
+
UNUserNotificationCenter.current().getNotificationSettings { st in
|
|
234
|
+
var b = 0
|
|
235
|
+
switch st.authorizationStatus {
|
|
236
|
+
case .authorized, .provisional, .ephemeral: b |= 1
|
|
237
|
+
default: break
|
|
238
|
+
}
|
|
239
|
+
if b & 1 != 0 {
|
|
240
|
+
if st.alertSetting == .enabled { b |= 2 }
|
|
241
|
+
if st.soundSetting == .enabled { b |= 4 }
|
|
242
|
+
if st.badgeSetting == .enabled { b |= 8 }
|
|
243
|
+
if st.lockScreenSetting == .enabled { b |= 16 }
|
|
244
|
+
if st.notificationCenterSetting == .enabled { b |= 32 }
|
|
245
|
+
}
|
|
246
|
+
bits = b
|
|
247
|
+
semaforo.signal()
|
|
248
|
+
}
|
|
249
|
+
_ = semaforo.wait(timeout: .now() + 0.5)
|
|
250
|
+
return bits
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Identificador de HARDWARE ("iPhone17,3") — não o nome comercial.
|
|
255
|
+
* `UIDevice.model` devolve só "iPhone"; o que distingue os modelos é o
|
|
256
|
+
* `utsname.machine`, e é dele que sai a tradução para nome comercial (feita
|
|
257
|
+
* no painel, não aqui: tabela de nomes envelhece e o aparelho não deve
|
|
258
|
+
* carregá-la).
|
|
259
|
+
*/
|
|
260
|
+
private static func modeloHardware() -> String {
|
|
261
|
+
#if targetEnvironment(simulator)
|
|
262
|
+
// No simulador o utsname devolve a arquitetura do Mac ("arm64"), não o
|
|
263
|
+
// modelo emulado — o modelo real vem desta env (verificado em bancada).
|
|
264
|
+
return ProcessInfo.processInfo.environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "Simulator"
|
|
265
|
+
#else
|
|
266
|
+
var info = utsname()
|
|
267
|
+
uname(&info)
|
|
268
|
+
return withUnsafeBytes(of: &info.machine) { buf in
|
|
269
|
+
guard let base = buf.baseAddress else { return UIDevice.current.model }
|
|
270
|
+
return String(cString: base.assumingMemoryBound(to: CChar.self))
|
|
271
|
+
}
|
|
272
|
+
#endif
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Wi-Fi × celular no instante do registro. Snapshot com espera CURTA (200 ms):
|
|
277
|
+
* o `NWPathMonitor` é assíncrono e não vale atrasar o registro por um campo
|
|
278
|
+
* de diagnóstico — sem resposta, o campo simplesmente não vai.
|
|
279
|
+
*/
|
|
280
|
+
private static func tipoDeRede() -> String? {
|
|
281
|
+
let monitor = NWPathMonitor()
|
|
282
|
+
let fila = DispatchQueue(label: "pushmesh.rede")
|
|
283
|
+
let semaforo = DispatchSemaphore(value: 0)
|
|
284
|
+
var resultado: String?
|
|
285
|
+
monitor.pathUpdateHandler = { path in
|
|
286
|
+
if path.status == .satisfied {
|
|
287
|
+
if path.usesInterfaceType(.wifi) {
|
|
288
|
+
resultado = "wifi"
|
|
289
|
+
} else if path.usesInterfaceType(.cellular) {
|
|
290
|
+
resultado = "celular"
|
|
291
|
+
} else {
|
|
292
|
+
resultado = "outro"
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
semaforo.signal()
|
|
296
|
+
}
|
|
297
|
+
monitor.start(queue: fila)
|
|
298
|
+
_ = semaforo.wait(timeout: .now() + 0.2)
|
|
299
|
+
monitor.cancel()
|
|
300
|
+
return resultado
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Heurística de jailbreak — SINAL, não prova (jailbreak moderno se esconde).
|
|
305
|
+
* Insumo antifraude; `false` significa "não achamos indício", nunca
|
|
306
|
+
* "aparelho confiável".
|
|
307
|
+
*/
|
|
308
|
+
private static func indicioDeJailbreak() -> Bool {
|
|
309
|
+
#if targetEnvironment(simulator)
|
|
310
|
+
return false // simulador tem sistema de arquivos aberto: daria falso positivo
|
|
311
|
+
#else
|
|
312
|
+
let caminhos = [
|
|
313
|
+
"/Applications/Cydia.app",
|
|
314
|
+
"/Applications/Sileo.app",
|
|
315
|
+
"/private/var/lib/apt/",
|
|
316
|
+
"/private/var/lib/cydia",
|
|
317
|
+
"/usr/sbin/sshd",
|
|
318
|
+
"/bin/bash",
|
|
319
|
+
"/etc/apt",
|
|
320
|
+
]
|
|
321
|
+
let fm = FileManager.default
|
|
322
|
+
if caminhos.contains(where: { fm.fileExists(atPath: $0) }) { return true }
|
|
323
|
+
// escrever fora do sandbox só é possível com jailbreak
|
|
324
|
+
let fora = "/private/pushmesh_teste.txt"
|
|
325
|
+
if (try? "x".write(toFile: fora, atomically: true, encoding: .utf8)) != nil {
|
|
326
|
+
try? fm.removeItem(atPath: fora)
|
|
327
|
+
return true
|
|
328
|
+
}
|
|
329
|
+
return false
|
|
330
|
+
#endif
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// MARK: - Callbacks vindos do swizzling
|
|
334
|
+
|
|
335
|
+
private func handleDidRegister(deviceToken: Data) {
|
|
336
|
+
let hex = deviceToken.map { String(format: "%02x", $0) }.joined()
|
|
337
|
+
let mudou = hex != apnsToken
|
|
338
|
+
apnsToken = hex
|
|
339
|
+
// token pendente SEMPRE persistido: rotação sem JS vivo é reconciliada
|
|
340
|
+
// no próximo boot (fallback do getToken).
|
|
341
|
+
UserDefaults.standard.set(hex, forKey: PushMeshApns.chaveTokenPendente)
|
|
342
|
+
let pend = pendentes
|
|
343
|
+
pendentes.removeAll()
|
|
344
|
+
pend.forEach { $0.resolve(hex) }
|
|
345
|
+
if mudou && hasListeners {
|
|
346
|
+
sendEvent(withName: "PushMeshApnsToken", body: hex)
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/// Foreground (willPresent) ou clique (didReceive) → evento JS; sem JS
|
|
351
|
+
/// vivo, o CLIQUE fica pendente (getPendingClick no boot) — o foreground
|
|
352
|
+
/// sem JS não existe (app fechado não tem willPresent).
|
|
353
|
+
private func emitirNotificacao(_ evento: String, userInfo: [AnyHashable: Any], pendenteSeSemListener: Bool) {
|
|
354
|
+
if hasListeners {
|
|
355
|
+
sendEvent(withName: evento, body: userInfo as NSDictionary)
|
|
356
|
+
} else if pendenteSeSemListener {
|
|
357
|
+
UserDefaults.standard.set(userInfo as NSDictionary, forKey: PushMeshApns.chaveClickPendente)
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private func handleDidFail(error: Error) {
|
|
362
|
+
rejeitarPendentes(
|
|
363
|
+
codigo: "pushmesh_apns_falhou",
|
|
364
|
+
mensagem: "[PushMesh] didFailToRegisterRemoteNotifications: \(error.localizedDescription). " +
|
|
365
|
+
"Como corrigir: confira a capability 'Push Notifications' no Xcode e use device físico.",
|
|
366
|
+
erro: error
|
|
367
|
+
)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private func rejeitarPendentes(codigo: String, mensagem: String, erro: Error?) {
|
|
371
|
+
let pend = pendentes
|
|
372
|
+
pendentes.removeAll()
|
|
373
|
+
pend.forEach { $0.reject(codigo, mensagem, erro) }
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// MARK: - Method swizzling do AppDelegate
|
|
377
|
+
|
|
378
|
+
private typealias DidRegisterIMP = @convention(c) (AnyObject, Selector, UIApplication, Data) -> Void
|
|
379
|
+
private typealias DidFailIMP = @convention(c) (AnyObject, Selector, UIApplication, NSError) -> Void
|
|
380
|
+
private typealias WillPresentIMP = @convention(c) (
|
|
381
|
+
AnyObject, Selector, UNUserNotificationCenter, UNNotification,
|
|
382
|
+
@convention(block) (UNNotificationPresentationOptions) -> Void
|
|
383
|
+
) -> Void
|
|
384
|
+
private typealias DidReceiveIMP = @convention(c) (
|
|
385
|
+
AnyObject, Selector, UNUserNotificationCenter, UNNotificationResponse,
|
|
386
|
+
@convention(block) () -> Void
|
|
387
|
+
) -> Void
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Espelho do `isNossa` do Android: a mensagem é NOSSA quando carrega alguma
|
|
391
|
+
* chave `pm_*` (bloco de exibição) ou `pm_*` (recibo/url). SEPARAÇÃO TOTAL
|
|
392
|
+
* (regra do produto): notificação de outro SDK atravessa o embrulho
|
|
393
|
+
* sem que a gente emita evento, persista clique ou abra URL — só o IMP
|
|
394
|
+
* original é encadeado.
|
|
395
|
+
*/
|
|
396
|
+
static func ehNossa(_ userInfo: [AnyHashable: Any]) -> Bool {
|
|
397
|
+
for chave in userInfo.keys {
|
|
398
|
+
if let k = chave as? String, k.hasPrefix("pmx_") || k.hasPrefix("pm_") { return true }
|
|
399
|
+
}
|
|
400
|
+
return false
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
static let chaveTokenPendente = "pmx_pending_token"
|
|
404
|
+
static let chaveClickPendente = "pmx_pending_click"
|
|
405
|
+
|
|
406
|
+
private static var ganchoSetDelegateFeito = false
|
|
407
|
+
/**
|
|
408
|
+
* Toques já processados: identifier → instante.
|
|
409
|
+
*
|
|
410
|
+
* POR QUE NÃO UM SLOT ÚNICO (era `ultimoToqueId` + `ultimoToqueEm`):
|
|
411
|
+
* guardando só o ÚLTIMO toque, duas notificações INTERCALADAS furavam o
|
|
412
|
+
* dedup. O usuário toca A e logo B; o encadeamento de delegates não é
|
|
413
|
+
* garantidamente síncrono (um SDK que despacha para o JS pode reentrar
|
|
414
|
+
* depois), então a reentrada de A chegava DEPOIS de B ter tomado o slot — A
|
|
415
|
+
* passava de novo e a `pm_url` abria DUAS vezes. O LRU do lado JS não cobre
|
|
416
|
+
* isso: ele dedupa RECIBO por `pm_msg_id`, enquanto a abertura da URL é 100%
|
|
417
|
+
* nativa (foi o sintoma real, visto em campo, que este dedup veio matar).
|
|
418
|
+
*
|
|
419
|
+
* É o espelho do LRU do JS, em miniatura: janela de 5 s, teto de 16 ids
|
|
420
|
+
* (rajada anormal não pode virar vazamento de memória) e poda a cada
|
|
421
|
+
* consulta. NSLock porque `willPresent`/`didReceive` não têm garantia
|
|
422
|
+
* DOCUMENTADA de fila única — barato e tira a dúvida.
|
|
423
|
+
*/
|
|
424
|
+
private static var toquesVistos: [String: TimeInterval] = [:]
|
|
425
|
+
private static let travaToques = NSLock()
|
|
426
|
+
private static let janelaToqueSegundos: TimeInterval = 5
|
|
427
|
+
private static let tetoToquesVistos = 16
|
|
428
|
+
|
|
429
|
+
/// true na PRIMEIRA passagem de um toque; false nas repetições do
|
|
430
|
+
/// encadeamento de delegates (janela de 5 s por identifier).
|
|
431
|
+
static func primeiraVezDoToque(_ id: String) -> Bool {
|
|
432
|
+
travaToques.lock()
|
|
433
|
+
defer { travaToques.unlock() }
|
|
434
|
+
let agora = Date().timeIntervalSince1970
|
|
435
|
+
// poda o que saiu da janela: o dicionário só guarda toque "quente"
|
|
436
|
+
toquesVistos = toquesVistos.filter { agora - $0.value < janelaToqueSegundos }
|
|
437
|
+
if toquesVistos[id] != nil { return false }
|
|
438
|
+
if toquesVistos.count >= tetoToquesVistos,
|
|
439
|
+
let maisAntigo = toquesVistos.min(by: { $0.value < $1.value })?.key {
|
|
440
|
+
toquesVistos.removeValue(forKey: maisAntigo)
|
|
441
|
+
}
|
|
442
|
+
toquesVistos[id] = agora
|
|
443
|
+
return true
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* O IMP original NUNCA mora numa estática única aqui — mora no
|
|
447
|
+
* `PushMeshSwizzleRegistro`, indexado por (classe, seletor), e cada bloco
|
|
448
|
+
* resolve pela classe em que ELE foi instalado (capturada na instalação).
|
|
449
|
+
* Pela classe do ALVO era quase certo — e caía no terceiro loop: subclasse
|
|
450
|
+
* que SOBRESCREVE o método e chama `super` (super → nosso bloco na
|
|
451
|
+
* ancestral → resolvendo pelo alvo achava o original da PRÓPRIA subclasse →
|
|
452
|
+
* sobrescrita de novo → ciclo).
|
|
453
|
+
*
|
|
454
|
+
* A LEI (dois defeitos reais que a estática única causava):
|
|
455
|
+
* - A→B→A: delegate da classe A embrulhado, host troca para B (a estática
|
|
456
|
+
* passava a apontar para o original de B) e depois volta para A. Ao
|
|
457
|
+
* re-embrulhar A, o "original" devolvido pelo runtime é O NOSSO PRÓPRIO
|
|
458
|
+
* BLOCO → o bloco chamava a si mesmo → recursão infinita → o app do
|
|
459
|
+
* cliente morre de stack overflow no primeiro push;
|
|
460
|
+
* - encadeamento entre SDKs: quem substitui o delegate costuma guardar o
|
|
461
|
+
* anterior e encaminhar a ele. Com a estática única, o bloco da classe A
|
|
462
|
+
* encadeava o IMP de B (corpo de outra classe com `self` de A) e fechava o
|
|
463
|
+
* ciclo B→A→B. Nem o ramo de dedup salvava: ele também encadeia o
|
|
464
|
+
* original.
|
|
465
|
+
*
|
|
466
|
+
* Estes quatro atalhos tipados existem só para converter o IMP cru do
|
|
467
|
+
* registro na assinatura C certa de cada callback.
|
|
468
|
+
*/
|
|
469
|
+
private static func originalDidRegister(instaladaEm classe: AnyClass) -> DidRegisterIMP? {
|
|
470
|
+
PushMeshSwizzleRegistro.original(paraClasse: classe, seletor: didRegisterSel)
|
|
471
|
+
.map { unsafeBitCast($0, to: DidRegisterIMP.self) }
|
|
472
|
+
}
|
|
473
|
+
private static func originalDidFail(instaladaEm classe: AnyClass) -> DidFailIMP? {
|
|
474
|
+
PushMeshSwizzleRegistro.original(paraClasse: classe, seletor: didFailSel)
|
|
475
|
+
.map { unsafeBitCast($0, to: DidFailIMP.self) }
|
|
476
|
+
}
|
|
477
|
+
private static func originalWillPresent(instaladaEm classe: AnyClass) -> WillPresentIMP? {
|
|
478
|
+
PushMeshSwizzleRegistro.original(paraClasse: classe, seletor: willPresentSel)
|
|
479
|
+
.map { unsafeBitCast($0, to: WillPresentIMP.self) }
|
|
480
|
+
}
|
|
481
|
+
private static func originalDidReceive(instaladaEm classe: AnyClass) -> DidReceiveIMP? {
|
|
482
|
+
PushMeshSwizzleRegistro.original(paraClasse: classe, seletor: didReceiveSel)
|
|
483
|
+
.map { unsafeBitCast($0, to: DidReceiveIMP.self) }
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
private static let didRegisterSel =
|
|
487
|
+
#selector(UIApplicationDelegate.application(_:didRegisterForRemoteNotificationsWithDeviceToken:))
|
|
488
|
+
// ATENÇÃO ao "For": o seletor real é
|
|
489
|
+
// `application:didFailToRegisterForRemoteNotificationsWithError:`. Sem o
|
|
490
|
+
// "For" o Swift não acha o membro no protocolo e a compilação do app do
|
|
491
|
+
// cliente quebra (não é aviso — é erro de build).
|
|
492
|
+
private static let didFailSel =
|
|
493
|
+
#selector(UIApplicationDelegate.application(_:didFailToRegisterForRemoteNotificationsWithError:))
|
|
494
|
+
private static let willPresentSel =
|
|
495
|
+
#selector(UNUserNotificationCenterDelegate.userNotificationCenter(_:willPresent:withCompletionHandler:))
|
|
496
|
+
private static let didReceiveSel =
|
|
497
|
+
#selector(UNUserNotificationCenterDelegate.userNotificationCenter(_:didReceive:withCompletionHandler:))
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Swizzling do delegate do UNUserNotificationCenter (foreground + clique) —
|
|
501
|
+
* mesmo cuidado do swizzling de registro: o IMP original (do notifee, que
|
|
502
|
+
* costuma ser o delegate, ou do host) é SEMPRE encadeado; sem delegate/
|
|
503
|
+
* original, o willPresent exibe banner+som e o didReceive só completa.
|
|
504
|
+
* Idempotente por CLASSE do delegate: se o host trocar o delegate depois
|
|
505
|
+
* (ex.: notifee se instalando), re-swizzla a classe nova.
|
|
506
|
+
*/
|
|
507
|
+
/**
|
|
508
|
+
* Ponto de entrada chamado do `+load` (PushMeshApnsModule.m) quando o app
|
|
509
|
+
* termina de abrir — ANTES de o sistema entregar o toque que abriu o app.
|
|
510
|
+
*
|
|
511
|
+
* Por que existe: o `UNUserNotificationCenter` só entrega
|
|
512
|
+
* `didReceive` a quem já é delegate quando o launch termina. Nosso swizzle
|
|
513
|
+
* acontecia na criação do módulo React (depois disso), então o TOQUE COM O
|
|
514
|
+
* APP FECHADO era perdido em silêncio — clique não contava e a `pm_url` não
|
|
515
|
+
* abria. Aqui garantimos presença desde o início.
|
|
516
|
+
*/
|
|
517
|
+
/**
|
|
518
|
+
* Abre a URL do clique — esperando o app estar ATIVO.
|
|
519
|
+
*
|
|
520
|
+
* `UIApplication.open` durante o launch é descartado pelo sistema. No toque
|
|
521
|
+
* com o app fechado o estado ainda é `.inactive`, então observamos
|
|
522
|
+
* `didBecomeActive` UMA vez e abrimos aí. Se já estiver ativo (toque com o
|
|
523
|
+
* app em segundo plano), abre na hora.
|
|
524
|
+
*/
|
|
525
|
+
@objc static func abrirUrlQuandoAtivo(_ endereco: String) {
|
|
526
|
+
let texto = endereco.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
527
|
+
guard !texto.isEmpty, let url = URL(string: texto) else { return }
|
|
528
|
+
|
|
529
|
+
let abrir = {
|
|
530
|
+
DispatchQueue.main.async {
|
|
531
|
+
NSLog("[PushMesh] abrindo pm_url: %@", texto)
|
|
532
|
+
// SEM canOpenURL DE PROPÓSITO (lei do zero-config): desde o iOS 9 o
|
|
533
|
+
// `canOpenURL` devolve false para QUALQUER esquema custom que o app
|
|
534
|
+
// host não tenha declarado em LSApplicationQueriesSchemes — inclusive
|
|
535
|
+
// o esquema do próprio app. Como o SDK promete não pedir nada no
|
|
536
|
+
// Info.plist do cliente, o guard reprovava todo deep link
|
|
537
|
+
// (`meuapp://tela`, que é o `app_url` do contrato do servidor) e o
|
|
538
|
+
// toque "não fazia nada" — só no iOS, e em silêncio. O `open` NÃO
|
|
539
|
+
// exige declaração alguma e já reporta o fracasso no completion: é lá
|
|
540
|
+
// que o erro ENSINA, e é o mesmo desenho do trampolim do Android.
|
|
541
|
+
UIApplication.shared.open(url, options: [:]) { ok in
|
|
542
|
+
if ok {
|
|
543
|
+
NSLog("[PushMesh] pm_url aberta: %@", texto)
|
|
544
|
+
} else {
|
|
545
|
+
NSLog("[PushMesh] pm_url NÃO abriu: %@ — nenhum app instalado atende o esquema \"%@\". " +
|
|
546
|
+
"Como corrigir: use https:// (abre no navegador/Universal Link) ou confira o " +
|
|
547
|
+
"esquema do deep link — ele precisa estar em CFBundleURLTypes do app que deve abrir.",
|
|
548
|
+
texto, url.scheme ?? "<sem esquema>")
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
DispatchQueue.main.async {
|
|
555
|
+
if UIApplication.shared.applicationState == .active {
|
|
556
|
+
abrir()
|
|
557
|
+
return
|
|
558
|
+
}
|
|
559
|
+
var token: NSObjectProtocol?
|
|
560
|
+
token = NotificationCenter.default.addObserver(
|
|
561
|
+
forName: UIApplication.didBecomeActiveNotification,
|
|
562
|
+
object: nil,
|
|
563
|
+
queue: .main
|
|
564
|
+
) { _ in
|
|
565
|
+
if let t = token { NotificationCenter.default.removeObserver(t) }
|
|
566
|
+
abrir()
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
@objc static func instalarSwizzleDelegate() {
|
|
572
|
+
DispatchQueue.main.async { swizzleNotificationDelegateSePreciso() }
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Gancho no `setDelegate:` do UNUserNotificationCenter — chamado do `+load`,
|
|
577
|
+
* antes de qualquer delegate existir.
|
|
578
|
+
*
|
|
579
|
+
* POR QUE ISTO É NECESSÁRIO: `swizzleNotificationDelegateSePreciso()` desiste
|
|
580
|
+
* quando ainda não há delegate (`guard let … else { return }`), e no arranque
|
|
581
|
+
* frio é exatamente esse o estado. O notifee/host define o delegate DEPOIS —
|
|
582
|
+
* e o toque que abriu o app já tinha se perdido. Interceptando a ATRIBUIÇÃO,
|
|
583
|
+
* embrulhamos o delegate no instante em que ele nasce, seja quem for e
|
|
584
|
+
* quando for. Foi este o defeito real de "no iOS vai direto pro app".
|
|
585
|
+
*/
|
|
586
|
+
@objc static func instalarGanchoSetDelegate() {
|
|
587
|
+
guard !ganchoSetDelegateFeito else { return }
|
|
588
|
+
ganchoSetDelegateFeito = true
|
|
589
|
+
|
|
590
|
+
let cls: AnyClass = UNUserNotificationCenter.self
|
|
591
|
+
let sel = NSSelectorFromString("setDelegate:")
|
|
592
|
+
guard let metodo = class_getInstanceMethod(cls, sel) else {
|
|
593
|
+
NSLog("[PushMesh] gancho setDelegate: método não encontrado")
|
|
594
|
+
return
|
|
595
|
+
}
|
|
596
|
+
typealias SetDelegateIMP = @convention(c) (AnyObject, Selector, AnyObject?) -> Void
|
|
597
|
+
let originalFn = unsafeBitCast(method_getImplementation(metodo), to: SetDelegateIMP.self)
|
|
598
|
+
|
|
599
|
+
let bloco: @convention(block) (AnyObject, AnyObject?) -> Void = { alvo, novoDelegate in
|
|
600
|
+
originalFn(alvo, sel, novoDelegate)
|
|
601
|
+
if novoDelegate != nil {
|
|
602
|
+
// o delegate acabou de nascer: embrulha AGORA
|
|
603
|
+
swizzleNotificationDelegateSePreciso()
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
method_setImplementation(metodo, imp_implementationWithBlock(bloco))
|
|
607
|
+
|
|
608
|
+
// Caso raro: alguém já definiu o delegate antes do nosso +load.
|
|
609
|
+
swizzleNotificationDelegateSePreciso()
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
static func swizzleNotificationDelegateSePreciso() {
|
|
613
|
+
guard let delegate = UNUserNotificationCenter.current().delegate else {
|
|
614
|
+
NSLog("[PushMesh] delegate de notificações ainda não existe — o gancho do setDelegate pega quando nascer")
|
|
615
|
+
return
|
|
616
|
+
}
|
|
617
|
+
let cls: AnyClass = type(of: delegate)
|
|
618
|
+
// Idempotente por CLASSE (não "pela última classe vista"): no A→B→A a
|
|
619
|
+
// classe A já está embrulhada e re-embrulhar capturaria o NOSSO bloco como
|
|
620
|
+
// original — recursão infinita. Quem sabe disso é o registro.
|
|
621
|
+
guard !PushMeshSwizzleRegistro.jaEmbrulhada(cls, willPresentSel)
|
|
622
|
+
|| !PushMeshSwizzleRegistro.jaEmbrulhada(cls, didReceiveSel) else { return }
|
|
623
|
+
NSLog("[PushMesh] delegate de notificações embrulhado: %@", NSStringFromClass(cls))
|
|
624
|
+
|
|
625
|
+
let blocoWill: @convention(block) (
|
|
626
|
+
AnyObject, UNUserNotificationCenter, UNNotification,
|
|
627
|
+
@convention(block) (UNNotificationPresentationOptions) -> Void
|
|
628
|
+
) -> Void = { alvo, center, notification, completion in
|
|
629
|
+
let infoWill = notification.request.content.userInfo
|
|
630
|
+
if PushMeshApns.ehNossa(infoWill) {
|
|
631
|
+
PushMeshApns.shared?.emitirNotificacao(
|
|
632
|
+
"PushMeshApnsMessage",
|
|
633
|
+
userInfo: infoWill,
|
|
634
|
+
pendenteSeSemListener: false
|
|
635
|
+
)
|
|
636
|
+
}
|
|
637
|
+
// o original é o da classe onde ESTE bloco foi instalado (capturada no
|
|
638
|
+
// embrulho) — nunca o do último delegate embrulhado, nem o da classe do
|
|
639
|
+
// alvo (subclasse que sobrescreve e chama super entraria em ciclo).
|
|
640
|
+
if let original = PushMeshApns.originalWillPresent(instaladaEm: cls) {
|
|
641
|
+
original(alvo, PushMeshApns.willPresentSel, center, notification, completion)
|
|
642
|
+
} else {
|
|
643
|
+
completion([.banner, .list, .sound])
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
PushMeshSwizzleRegistro.instalar(classe: cls, seletor: willPresentSel, tipos: "v@:@@@",
|
|
647
|
+
nossoImp: imp_implementationWithBlock(blocoWill))
|
|
648
|
+
|
|
649
|
+
let blocoClick: @convention(block) (
|
|
650
|
+
AnyObject, UNUserNotificationCenter, UNNotificationResponse,
|
|
651
|
+
@convention(block) () -> Void
|
|
652
|
+
) -> Void = { alvo, center, response, completion in
|
|
653
|
+
// SEPARAÇÃO TOTAL: toque em notificação de OUTRO SDK não é assunto nosso
|
|
654
|
+
// — nada de evento, recibo, clique pendente ou URL. Só encadeia.
|
|
655
|
+
guard PushMeshApns.ehNossa(response.notification.request.content.userInfo) else {
|
|
656
|
+
if let original = PushMeshApns.originalDidReceive(instaladaEm: cls) {
|
|
657
|
+
original(alvo, PushMeshApns.didReceiveSel, center, response, completion)
|
|
658
|
+
} else {
|
|
659
|
+
completion()
|
|
660
|
+
}
|
|
661
|
+
return
|
|
662
|
+
}
|
|
663
|
+
// DEDUP do toque: os delegates embrulhados se ENCADEIAM (um SDK de push
|
|
664
|
+
// costuma chamar o delegate anterior, e cada classe passa pelo bloco) — sem isto o
|
|
665
|
+
// mesmo toque processava DUAS vezes: duas aberturas de URL e dois
|
|
666
|
+
// recibos (visto em teste de campo). O original é SEMPRE
|
|
667
|
+
// encadeado; só o NOSSO processamento é único por toque.
|
|
668
|
+
guard PushMeshApns.primeiraVezDoToque(response.notification.request.identifier) else {
|
|
669
|
+
if let original = PushMeshApns.originalDidReceive(instaladaEm: cls) {
|
|
670
|
+
original(alvo, PushMeshApns.didReceiveSel, center, response, completion)
|
|
671
|
+
} else {
|
|
672
|
+
completion()
|
|
673
|
+
}
|
|
674
|
+
return
|
|
675
|
+
}
|
|
676
|
+
// O toque pode chegar ANTES de o módulo React existir (arranque frio:
|
|
677
|
+
// o swizzle é instalado no +load, mas `shared` só nasce quando a ponte
|
|
678
|
+
// RN cria os módulos). Sem esta guarda o clique se perdia em silêncio —
|
|
679
|
+
// era este o caso do "iOS não aponta para o link".
|
|
680
|
+
let userInfo = response.notification.request.content.userInfo
|
|
681
|
+
|
|
682
|
+
// pm_url é aberta AQUI, no nativo — não no JS. Duas razões:
|
|
683
|
+
// 1. no arranque frio o JS pode nem existir ainda quando o toque chega;
|
|
684
|
+
// 2. `Linking.openURL` durante o launch é descartado pelo iOS em
|
|
685
|
+
// silêncio (o app abria e o link não).
|
|
686
|
+
// O nativo espera o app ficar ATIVO e abre com UIApplication.open, que é
|
|
687
|
+
// o mesmo caminho que o trampolim do Android usa.
|
|
688
|
+
NSLog("[PushMesh] TOQUE recebido no nativo (pm_url=%@)",
|
|
689
|
+
(userInfo["pm_url"] as? String) ?? "<sem url>")
|
|
690
|
+
if let url = userInfo["pm_url"] as? String {
|
|
691
|
+
PushMeshApns.abrirUrlQuandoAtivo(url)
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if let instancia = PushMeshApns.shared {
|
|
695
|
+
instancia.emitirNotificacao(
|
|
696
|
+
"PushMeshApnsClick",
|
|
697
|
+
userInfo: userInfo,
|
|
698
|
+
pendenteSeSemListener: true
|
|
699
|
+
)
|
|
700
|
+
} else {
|
|
701
|
+
UserDefaults.standard.set(userInfo as NSDictionary, forKey: PushMeshApns.chaveClickPendente)
|
|
702
|
+
}
|
|
703
|
+
if let original = PushMeshApns.originalDidReceive(instaladaEm: cls) {
|
|
704
|
+
original(alvo, PushMeshApns.didReceiveSel, center, response, completion)
|
|
705
|
+
} else {
|
|
706
|
+
completion()
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
PushMeshSwizzleRegistro.instalar(classe: cls, seletor: didReceiveSel, tipos: "v@:@@@",
|
|
710
|
+
nossoImp: imp_implementationWithBlock(blocoClick))
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
static func swizzleAppDelegate() {
|
|
714
|
+
guard let delegate = UIApplication.shared.delegate else {
|
|
715
|
+
NSLog("[PushMesh] AppDelegate ausente na 1ª instanciação — swizzling APNs não instalado; " +
|
|
716
|
+
"getToken pode falhar neste boot.")
|
|
717
|
+
return
|
|
718
|
+
}
|
|
719
|
+
let cls: AnyClass = type(of: delegate)
|
|
720
|
+
// idempotência por CLASSE (era um `swizzleFeito` global): se o AppDelegate
|
|
721
|
+
// trocar de classe, a classe nova também é embrulhada — e a antiga
|
|
722
|
+
// continua com o encadeamento certo, porque o original mora por classe.
|
|
723
|
+
guard !PushMeshSwizzleRegistro.jaEmbrulhada(cls, didRegisterSel)
|
|
724
|
+
|| !PushMeshSwizzleRegistro.jaEmbrulhada(cls, didFailSel) else { return }
|
|
725
|
+
|
|
726
|
+
// didRegisterForRemoteNotificationsWithDeviceToken
|
|
727
|
+
let blocoReg: @convention(block) (AnyObject, UIApplication, Data) -> Void = { alvo, app, deviceToken in
|
|
728
|
+
PushMeshApns.shared?.handleDidRegister(deviceToken: deviceToken)
|
|
729
|
+
if let original = PushMeshApns.originalDidRegister(instaladaEm: cls) {
|
|
730
|
+
original(alvo, PushMeshApns.didRegisterSel, app, deviceToken)
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
PushMeshSwizzleRegistro.instalar(classe: cls, seletor: didRegisterSel, tipos: "v@:@@",
|
|
734
|
+
nossoImp: imp_implementationWithBlock(blocoReg))
|
|
735
|
+
|
|
736
|
+
// didFailToRegisterRemoteNotificationsWithError
|
|
737
|
+
let blocoErro: @convention(block) (AnyObject, UIApplication, NSError) -> Void = { alvo, app, erro in
|
|
738
|
+
PushMeshApns.shared?.handleDidFail(error: erro)
|
|
739
|
+
if let original = PushMeshApns.originalDidFail(instaladaEm: cls) {
|
|
740
|
+
original(alvo, PushMeshApns.didFailSel, app, erro)
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
PushMeshSwizzleRegistro.instalar(classe: cls, seletor: didFailSel, tipos: "v@:@@",
|
|
744
|
+
nossoImp: imp_implementationWithBlock(blocoErro))
|
|
745
|
+
}
|
|
746
|
+
}
|