@pushmesh/sdk 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +160 -0
  3. package/android/build.gradle +82 -0
  4. package/android/src/main/AndroidManifest.xml +69 -0
  5. package/android/src/main/java/io/pushmesh/sdk/PushMeshClickActivity.kt +124 -0
  6. package/android/src/main/java/io/pushmesh/sdk/PushMeshFirebaseModule.kt +512 -0
  7. package/android/src/main/java/io/pushmesh/sdk/PushMeshFirebasePackage.kt +20 -0
  8. package/android/src/main/java/io/pushmesh/sdk/PushMeshMessageBuffer.kt +53 -0
  9. package/android/src/main/java/io/pushmesh/sdk/PushMeshMessageRouter.kt +477 -0
  10. package/android/src/main/java/io/pushmesh/sdk/PushMeshMessagingReceiver.kt +77 -0
  11. package/android/src/main/java/io/pushmesh/sdk/PushMeshMessagingService.kt +88 -0
  12. package/dist/cjs/boot.d.ts +7 -0
  13. package/dist/cjs/boot.js +1 -0
  14. package/dist/cjs/compat.d.ts +63 -0
  15. package/dist/cjs/compat.js +1 -0
  16. package/dist/cjs/doctor.d.ts +27 -0
  17. package/dist/cjs/doctor.js +2 -0
  18. package/dist/cjs/events.d.ts +21 -0
  19. package/dist/cjs/events.js +1 -0
  20. package/dist/cjs/http.d.ts +20 -0
  21. package/dist/cjs/http.js +1 -0
  22. package/dist/cjs/inapp/SkeletonShimmer.d.ts +1 -0
  23. package/dist/cjs/inapp/SkeletonShimmer.js +1 -0
  24. package/dist/cjs/inapp/auto.d.ts +1 -0
  25. package/dist/cjs/inapp/auto.js +1 -0
  26. package/dist/cjs/inapp/components.d.ts +14 -0
  27. package/dist/cjs/inapp/components.js +1 -0
  28. package/dist/cjs/inapp/engine.d.ts +1 -0
  29. package/dist/cjs/inapp/engine.js +1 -0
  30. package/dist/cjs/inapp/imageCache.d.ts +1 -0
  31. package/dist/cjs/inapp/imageCache.js +1 -0
  32. package/dist/cjs/inapp/index.d.ts +20 -0
  33. package/dist/cjs/inapp/index.js +1 -0
  34. package/dist/cjs/inapp/lifecycle.d.ts +1 -0
  35. package/dist/cjs/inapp/lifecycle.js +1 -0
  36. package/dist/cjs/inapp/normalize.d.ts +1 -0
  37. package/dist/cjs/inapp/normalize.js +1 -0
  38. package/dist/cjs/inapp/registro.d.ts +1 -0
  39. package/dist/cjs/inapp/registro.js +1 -0
  40. package/dist/cjs/inapp/sessao.d.ts +1 -0
  41. package/dist/cjs/inapp/sessao.js +1 -0
  42. package/dist/cjs/index.d.ts +118 -0
  43. package/dist/cjs/index.js +1 -0
  44. package/dist/cjs/perfil.d.ts +52 -0
  45. package/dist/cjs/perfil.js +1 -0
  46. package/dist/cjs/permissions.d.ts +54 -0
  47. package/dist/cjs/permissions.js +1 -0
  48. package/dist/cjs/players.d.ts +20 -0
  49. package/dist/cjs/players.js +1 -0
  50. package/dist/cjs/queue.d.ts +1 -0
  51. package/dist/cjs/queue.js +1 -0
  52. package/dist/cjs/receipts.d.ts +32 -0
  53. package/dist/cjs/receipts.js +1 -0
  54. package/dist/cjs/state.d.ts +21 -0
  55. package/dist/cjs/state.js +1 -0
  56. package/dist/cjs/storage.d.ts +1 -0
  57. package/dist/cjs/storage.js +1 -0
  58. package/dist/cjs/tags.d.ts +13 -0
  59. package/dist/cjs/tags.js +1 -0
  60. package/dist/cjs/triggers.d.ts +22 -0
  61. package/dist/cjs/triggers.js +1 -0
  62. package/dist/cjs/types.d.ts +258 -0
  63. package/dist/cjs/types.js +1 -0
  64. package/dist/cjs/zeroconfig.d.ts +45 -0
  65. package/dist/cjs/zeroconfig.js +1 -0
  66. package/ios/NotificationService/LEIA-ME.md +105 -0
  67. package/ios/NotificationService/PushMeshNotificationService.swift +384 -0
  68. package/ios/PushMeshApns.swift +746 -0
  69. package/ios/PushMeshApnsModule.m +76 -0
  70. package/ios/PushMeshSwizzleRegistro.swift +141 -0
  71. package/package.json +113 -0
  72. package/pushmesh-sdk.podspec +32 -0
  73. package/react-native.config.js +17 -0
@@ -0,0 +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.payloadHash=payloadHash,exports.buildRegisterPayload=buildRegisterPayload,exports.__resetRegistroForTests=__resetRegistroForTests,exports.register=register,exports.login=login,exports.logout=logout,exports.init=init,exports.setToken=setToken,exports.getPlayerId=getPlayerId;const react_native_1=require("react-native"),http_1=require("./http"),perfil_1=require("./perfil"),queue_1=require("./queue"),state_1=require("./state"),storage_1=require("./storage"),types_1=require("./types"),GERACAO_REGISTRO=5;function payloadHash(payload){const{sessoes:_sessoes,...semSessoes}=payload,dia=(new Date).toISOString().slice(0,10),s=`g${GERACAO_REGISTRO}|d${dia}|${JSON.stringify(semSessoes,Object.keys(semSessoes).sort())}`;let h=2166136261;for(let i=0;i<s.length;i++)h^=s.charCodeAt(i),h=Math.imul(h,16777619);return(h>>>0).toString(16)}function resolveDeviceType(config){return config.deviceType&&"auto"!==config.deviceType?config.deviceType:"ios"===react_native_1.Platform.OS?types_1.RpDeviceType.IOS:types_1.RpDeviceType.Android}function resolveDeviceModel(config){if(config.deviceModel)return config.deviceModel;const constants=react_native_1.Platform.constants;return constants?.Model}function resolveLanguage(){try{return Intl.DateTimeFormat().resolvedOptions().locale}catch{return}}function resolveTimezoneOffsetSegundos(){try{const off=60*-(new Date).getTimezoneOffset();return Number.isFinite(off)?off:void 0}catch{return}}function buildRegisterPayload(){const config=(0,state_1.requireConfig)();if(!state_1.state.token)throw new Error("[PushMesh] sem token de push. Como corrigir: passe config.getToken no init ou chame PushMesh.setToken(token) quando o Firebase entregar o token.");const payload={app_id:config.appId,identifier:state_1.state.token,device_type:resolveDeviceType(config)};state_1.state.transporte&&(payload.transporte=state_1.state.transporte),state_1.state.externalUserId?payload.external_user_id=state_1.state.externalUserId:state_1.state.logoutPendente&&(payload.external_user_id=null),config.appVersion&&(payload.app_version=config.appVersion);const model=resolveDeviceModel(config);model&&(payload.device_model=model);const language=resolveLanguage();language&&(payload.language=language);const timezone=resolveTimezoneOffsetSegundos();void 0!==timezone&&(payload.timezone=timezone);const{modelo:modeloPerfil,...perfilSemModelo}=state_1.state.perfil;return Object.assign(payload,perfilSemModelo),!payload.device_model&&modeloPerfil&&(payload.device_model=modeloPerfil),payload}let registroEmVoo=null;function __resetRegistroForTests(){registroEmVoo=null}async function register(){const hash=payloadHash(buildRegisterPayload());if(registroEmVoo?.hash===hash)return registroEmVoo.promessa;const entrada={hash:hash,promessa:(registroEmVoo?.promessa??Promise.resolve(null)).then(registrarAgora,registrarAgora)};registroEmVoo=entrada;const liberar=()=>{registroEmVoo===entrada&&(registroEmVoo=null)};return entrada.promessa.then(liberar,liberar),entrada.promessa}async function registrarAgora(){const payload=buildRegisterPayload(),hash=payloadHash(payload),cachedHash=await(0,storage_1.getItem)(storage_1.KEYS.regHash),cachedPlayerId=await(0,storage_1.getItem)(storage_1.KEYS.playerId);if(cachedHash===hash&&cachedPlayerId)return state_1.state.playerId=cachedPlayerId,cachedPlayerId;const res=await(0,http_1.post)("/api/v1/players",payload);if(!res.ok)return console.warn(`[PushMesh] POST /players respondeu ${res.status}`),null;const body=res.json;if(!body?.id)return console.warn("[PushMesh] POST /players sem id na resposta"),null;state_1.state.playerId=body.id,await(0,storage_1.setItem)(storage_1.KEYS.playerId,body.id),await(0,storage_1.setItem)(storage_1.KEYS.regHash,hash),state_1.state.logoutPendente&&(state_1.state.logoutPendente=false,await(0,storage_1.removeItem)(storage_1.KEYS.logoutPendente));const{sincronizarReciboNativo:sincronizarReciboNativo}=await Promise.resolve().then(()=>__importStar(require("./zeroconfig")));await sincronizarReciboNativo(body.id).catch(()=>{});const{syncTagsPendentes:syncTagsPendentes}=await Promise.resolve().then(()=>__importStar(require("./tags")));return await syncTagsPendentes().catch(()=>{}),body.id}async function login(externalUserId){(0,state_1.requireConfig)(),state_1.state.externalUserId=externalUserId,await(0,storage_1.setItem)(storage_1.KEYS.externalUserId,externalUserId),state_1.state.logoutPendente=false,await(0,storage_1.removeItem)(storage_1.KEYS.logoutPendente);const playerId=await ensurePlayerId();playerId&&await(0,queue_1.sendOrQueue)("PUT",`/api/v1/players/${playerId}`,{app_id:(0,state_1.requireConfig)().appId,external_user_id:externalUserId})}async function logout(){(0,state_1.requireConfig)(),state_1.state.externalUserId=null,await(0,storage_1.removeItem)(storage_1.KEYS.externalUserId),state_1.state.logoutPendente=true,await(0,storage_1.setItem)(storage_1.KEYS.logoutPendente,"1");const playerId=state_1.state.playerId??await(0,storage_1.getItem)(storage_1.KEYS.playerId);playerId&&await(0,queue_1.sendOrQueue)("PUT",`/api/v1/players/${playerId}`,{app_id:(0,state_1.requireConfig)().appId,external_user_id:null})}async function ensurePlayerId(){if(state_1.state.playerId)return state_1.state.playerId;const cached=await(0,storage_1.getItem)(storage_1.KEYS.playerId);return cached&&(state_1.state.playerId=cached),cached}function textoOutroAppId(anterior,novo){return`[PushMesh] init com appId ${novo}, mas o SDK já está inicializado com ${anterior}. Como corrigir: o SDK é singleton do processo — use UM appId no boot do app.`}async function init(config){if(!config.appId)throw new Error("[PushMesh] config.appId é obrigatório.");if(state_1.state.initEmVoo){if(state_1.state.config&&state_1.state.config.appId!==config.appId)throw new Error(textoOutroAppId(state_1.state.config.appId,config.appId));return state_1.state.initEmVoo}const promessa=bootstrap(config);state_1.state.initEmVoo=promessa;try{await promessa}finally{state_1.state.initEmVoo=null}}async function bootstrap(config){state_1.state.config&&state_1.state.config.appId!==config.appId&&console.warn(textoOutroAppId(state_1.state.config.appId,config.appId));const baseUrl=config.baseUrl?.trim()||state_1.BASE_URL_PADRAO;if((0,state_1.validarBaseUrl)(baseUrl),state_1.state.config={...config,baseUrl:baseUrl},config=state_1.state.config,state_1.state.perfil=await(0,perfil_1.coletarPerfil)(),state_1.state.playerId=await(0,storage_1.getItem)(storage_1.KEYS.playerId),state_1.state.token=await(0,storage_1.getItem)(storage_1.KEYS.token),state_1.state.transporte="apns"===await(0,storage_1.getItem)(storage_1.KEYS.transporte)?"apns":null,state_1.state.externalUserId=await(0,storage_1.getItem)(storage_1.KEYS.externalUserId),state_1.state.logoutPendente="1"===await(0,storage_1.getItem)(storage_1.KEYS.logoutPendente),config.externalUserId&&config.externalUserId!==state_1.state.externalUserId&&await login(config.externalUserId).catch(e=>console.warn("[PushMesh] externalUserId do init não propagou agora — vai no registro",e)),config.getToken){state_1.state.transporte&&(state_1.state.transporte=null,await(0,storage_1.removeItem)(storage_1.KEYS.transporte));try{const token=await config.getToken();token&&token!==state_1.state.token&&(state_1.state.token=token,await(0,storage_1.setItem)(storage_1.KEYS.token,token))}catch(e){console.warn("[PushMesh] config.getToken falhou",e)}}else if("android"===react_native_1.Platform.OS){const{bootZeroConfigAndroid:bootZeroConfigAndroid}=await Promise.resolve().then(()=>__importStar(require("./zeroconfig")));await bootZeroConfigAndroid()}else{const{bootZeroConfigIos:bootZeroConfigIos}=await Promise.resolve().then(()=>__importStar(require("./zeroconfig")));await bootZeroConfigIos()}if(state_1.state.token)try{await register()}catch(e){console.warn("[PushMesh] registro falhou (rede?) — retenta no próximo boot",e)}if(!config.getToken)try{const zc=await Promise.resolve().then(()=>__importStar(require("./zeroconfig")));"android"===react_native_1.Platform.OS?(await zc.drenarMensagensPendentes(),await zc.processarCliqueInicial()):await zc.processarCliquePendenteIos()}catch(e){console.warn("[PushMesh] falha no pós-registro do zero-config (buffer/clique)",e)}await(0,queue_1.flushQueue)().catch(()=>{})}async function setToken(token){if(token&&token!==state_1.state.token&&(state_1.state.token=token,await(0,storage_1.setItem)(storage_1.KEYS.token,token),state_1.state.config))try{await register()}catch(e){console.warn("[PushMesh] registro pós-setToken falhou",e)}}async function getPlayerId(){return ensurePlayerId()}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true}),exports.FLUSH_CONCURRENCY=void 0,exports.__resetQueueForTests=__resetQueueForTests,exports.__drainQueueLockForTests=__drainQueueLockForTests,exports.backoffMs=backoffMs,exports.enqueue=enqueue,exports.sendOrQueue=sendOrQueue,exports.flushQueue=flushQueue;const http_1=require("./http"),storage_1=require("./storage"),state_1=require("./state"),MAX_ATTEMPTS=10,MAX_QUEUE_ITEMS=500,BASE_DELAY_MS=1e3,MAX_DELAY_MS=3e5,JITTER_MS=1e3;exports.FLUSH_CONCURRENCY=5;let localId=0,ultimoNascimento=0;function agoraMonotonico(){return ultimoNascimento=Math.max(Date.now(),ultimoNascimento+1),ultimoNascimento}function nextId(){return localId+=1,`${agoraMonotonico()}-${localId}`}function criadoEm(item){return Number(item.id.split("-")[0])||0}function chaveEstado(item){if("PUT"!==item.method)return null;const campos=Object.keys(item.body).filter(c=>"app_id"!==c).sort().join(",");return`${item.path}#${campos}`}const superadoEm=new Map;function marcarSuperado(chave,quando){chave&&quando>(superadoEm.get(chave)??0)&&superadoEm.set(chave,quando)}function estaSuperado(item){const chave=chaveEstado(item);return null!==chave&&(superadoEm.get(chave)??0)>criadoEm(item)}function avisaDescarte(item,motivo){console.warn(`[PushMesh] ${item.method} ${item.path} descartado — ${motivo}`)}function coalescer(itens){const posicao=new Map,saida=[];for(const item of itens){if(estaSuperado(item))continue;const chave=chaveEstado(item);if(null===chave){saida.push(item);continue}const idx=posicao.get(chave);void 0===idx?posicao.set(chave,saida.push(item)-1):criadoEm(item)>=criadoEm(saida[idx])&&(saida[idx]=item)}return saida}let naoPersistidos=[];function __resetQueueForTests(){superadoEm.clear(),naoPersistidos=[]}function guardarEmMemoria(itens){naoPersistidos.push(...itens),naoPersistidos.length>MAX_QUEUE_ITEMS&&naoPersistidos.splice(0,naoPersistidos.length-MAX_QUEUE_ITEMS)}let cadeia=Promise.resolve();function comTrava(op){const proxima=cadeia.then(op,op);return cadeia=proxima.then(()=>{},()=>{}),proxima}async function __drainQueueLockForTests(){await cadeia}function backoffMs(attempts,jitter=Math.random){return Math.min(BASE_DELAY_MS*2**attempts,MAX_DELAY_MS)+Math.floor(jitter()*JITTER_MS)}async function enqueue(method,path,body,delayMs=0){const novo={id:nextId(),method:method,path:path,body:body,attempts:0,nextAttemptAt:Date.now()+delayMs};marcarSuperado(chaveEstado(novo),criadoEm(novo)),await comTrava(async()=>{const{valor:valor,falhou:falhou}=await(0,storage_1.getJsonSeguro)(storage_1.KEYS.queue,[]);if(falhou)return guardarEmMemoria([novo]),void console.warn("[PushMesh] fila ilegível agora (storage ocupado) — item guardado em memória");const queue=coalescer([...valor,...naoPersistidos,novo]);if(naoPersistidos=[],queue.length>MAX_QUEUE_ITEMS){const descartados=queue.length-MAX_QUEUE_ITEMS;queue.splice(0,descartados),console.warn(`[PushMesh] fila cheia (teto ${MAX_QUEUE_ITEMS}) — ${descartados} item(ns) antigo(s) descartado(s)`)}await(0,storage_1.setJson)(storage_1.KEYS.queue,queue)})}async function sendOrQueue(method,path,body){const nascimento=agoraMonotonico();try{const res=await(0,http_1.request)(method,path,body);return 429===res.status||res.status>=500?(await enqueue(method,path,body,res.retryAfterMs??backoffMs(1)),true):res.ok?(marcarSuperado(chaveEstado({method:method,path:path,body:body}),nascimento),await purgarSuperados(),true):(avisaDescarte({method:method,path:path},`${res.status}: retry não corrige payload inválido`),false)}catch(e){if(e instanceof http_1.NetworkError)return await enqueue(method,path,body),true;throw e}}async function purgarSuperados(){await comTrava(async()=>{const{valor:valor,falhou:falhou}=await(0,storage_1.getJsonSeguro)(storage_1.KEYS.queue,[]);if(!falhou&&valor.length>0){const vivos=valor.filter(item=>!estaSuperado(item));vivos.length!==valor.length&&await(0,storage_1.setJson)(storage_1.KEYS.queue,vivos)}const{valor:emVoo,falhou:falhouVoo}=await(0,storage_1.getJsonSeguro)(storage_1.KEYS.queueInflight,[]);if(!falhouVoo&&emVoo.length>0){const vivosVoo=emVoo.filter(item=>!estaSuperado(item));vivosVoo.length!==emVoo.length&&await(0,storage_1.setJson)(storage_1.KEYS.queueInflight,vivosVoo)}})}async function resolverPlayer(item){if(null!==item.body.player_id)return true;const playerId=state_1.state.playerId??await(0,storage_1.getItem)(storage_1.KEYS.playerId);return!!playerId&&(item.body={...item.body,player_id:playerId},true)}async function processItem(item,now){const retry=(delayMs,attempts)=>attempts<10?{...item,attempts:attempts,nextAttemptAt:now+delayMs}:null;if(estaSuperado(item))return avisaDescarte(item,"escrita mais nova do mesmo campo já foi enviada"),null;if(!await resolverPlayer(item))return retry(backoffMs(item.attempts+1),item.attempts+1);try{const res=await(0,http_1.request)(item.method,item.path,item.body);return res.ok?null:429===res.status||res.status>=500?retry(res.retryAfterMs??backoffMs(item.attempts+1),item.attempts+1):(avisaDescarte(item,`${res.status}: retry não corrige payload inválido`),null)}catch{return retry(backoffMs(item.attempts+1),item.attempts+1)}}async function flushQueue(now=Date.now()){if(!state_1.state.flushing){state_1.state.flushing=true;try{const due=await comTrava(async()=>{const{valor:pendentes,falhou:falhou}=await(0,storage_1.getJsonSeguro)(storage_1.KEYS.queue,[]);if(falhou)return[];const{valor:emVoo,falhou:falhouVoo}=await(0,storage_1.getJsonSeguro)(storage_1.KEYS.queueInflight,[]);if(falhouVoo)return[];if(pendentes.length+emVoo.length+naoPersistidos.length===0)return[];const queue=coalescer([...emVoo,...pendentes,...naoPersistidos]);naoPersistidos=[];const prontos=queue.filter(item=>item.nextAttemptAt<=now),adiados=queue.filter(item=>item.nextAttemptAt>now);return await(0,storage_1.setJson)(storage_1.KEYS.queue,adiados),await(0,storage_1.setJson)(storage_1.KEYS.queueInflight,prontos),prontos});if(0===due.length)return;const devolver=[];let cursor=0;const worker=async()=>{for(;cursor<due.length;){const item=due[cursor++];try{const kept=await processItem(item,now);kept&&devolver.push(kept)}catch{devolver.push({...item,attempts:item.attempts+1,nextAttemptAt:now+backoffMs(item.attempts+1)})}}};await Promise.all(Array.from({length:Math.min(exports.FLUSH_CONCURRENCY,due.length)},()=>worker())),await comTrava(async()=>{const{valor:atual,falhou:falhou}=await(0,storage_1.getJsonSeguro)(storage_1.KEYS.queue,[]);if(falhou)return void guardarEmMemoria(devolver);const mesclado=coalescer([...devolver,...atual]);mesclado.length>MAX_QUEUE_ITEMS&&mesclado.splice(0,mesclado.length-MAX_QUEUE_ITEMS),await(0,storage_1.setJson)(storage_1.KEYS.queue,mesclado),await(0,storage_1.setJson)(storage_1.KEYS.queueInflight,[])})}finally{state_1.state.flushing=false}}}
@@ -0,0 +1,32 @@
1
+ /** Apenas para testes: limpa o cache em memória (o storage é mockado à parte). */
2
+ export declare function __resetLruForTests(): void;
3
+ /** Eventos aceitos pelo servidor em POST /receipts. */
4
+ export type EventoRecibo = 'recebido' | 'clique';
5
+ /**
6
+ * Envia o recibo. Fire-and-forget: falha de rede/5xx vai para a fila offline e é
7
+ * reenviada no próximo init/foreground. Nunca duplica (LRU-64).
8
+ *
9
+ * O `evento` entra na CHAVE do dedup: entrega e clique são fatos distintos da
10
+ * MESMA notificação. Com a chave só no notification_id, o clique batia no
11
+ * "já visto" (a entrega sempre passa antes) e o POST nem saía — o painel
12
+ * mostrava cliques zerados para sempre. Defeito real, visto em campo.
13
+ *
14
+ * ORDEM É LEI (2026-08-01): o "já visto" só é marcado DEPOIS de o envio ser aceito
15
+ * (entregue ou guardado na fila). Devolve true quando o recibo foi aceito.
16
+ */
17
+ export declare function sendReceipt(notificationId: string, rcpt: string, evento?: EventoRecibo): Promise<boolean>;
18
+ /**
19
+ * Ponto de entrada: chame com o `data` da mensagem FCM/APNs recebida
20
+ * (foreground onMessage, background handler e notificação que abriu o app).
21
+ * No-op silencioso para pushes que não são do PUSHMESH (sem pm_msg_id).
22
+ *
23
+ * Dispara o evento 'foregroundWillDisplay' para os listeners registrados
24
+ * (dedup pelo LRU: duplicata não dispara 2x para pushes PUSHMESH).
25
+ */
26
+ export declare function handlePushData(data: unknown): Promise<boolean>;
27
+ /**
28
+ * Usuário tocou na notificação: dispara 'click' para os listeners e garante
29
+ * o recibo de entrega (dedup interno — se o foreground já recebeu, não repete).
30
+ * Chame no getInitialNotification / onNotificationOpenedApp do app host.
31
+ */
32
+ export declare function handleClick(data: unknown): Promise<void>;
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true}),exports.__resetLruForTests=__resetLruForTests,exports.sendReceipt=sendReceipt,exports.handlePushData=handlePushData,exports.handleClick=handleClick;const react_native_1=require("react-native"),events_1=require("./events"),queue_1=require("./queue"),state_1=require("./state"),storage_1=require("./storage"),LRU_SIZE=64;let lru=null;async function loadLru(){if(lru)return lru;const{valor:valor,falhou:falhou}=await(0,storage_1.getJsonSeguro)(storage_1.KEYS.receiptLru,[]);return falhou||(lru=valor),valor}async function markSeen(msgId){const list=await loadLru(),idx=list.indexOf(msgId);for(idx>=0&&list.splice(idx,1),list.push(msgId);list.length>64;)list.shift();lru===list&&await(0,storage_1.setJson)(storage_1.KEYS.receiptLru,list)}async function wasSeen(msgId){return(await loadLru()).includes(msgId)}function __resetLruForTests(){lru=null}async function sendReceipt(notificationId,rcpt,evento="recebido"){const chave="recebido"===evento?notificationId:`${notificationId}#${evento}`;if(await wasSeen(chave))return false;const config=(0,state_1.requireConfig)(),playerId=state_1.state.playerId??await(0,storage_1.getItem)(storage_1.KEYS.playerId),corpo={app_id:config.appId,notification_id:notificationId,player_id:playerId,rcpt:rcpt,evento:evento};let aceito;return playerId?aceito=await(0,queue_1.sendOrQueue)("POST","/api/v1/receipts",corpo):(console.warn("[PushMesh] recibo antes do registro — guardado na fila até existir player_id"),await(0,queue_1.enqueue)("POST","/api/v1/receipts",corpo),aceito=true),aceito&&await markSeen(chave),aceito}async function handlePushData(data){if(!data||"object"!=typeof data)return false;const d=data,msgId="string"==typeof d.pm_msg_id?d.pm_msg_id:null,rcpt="string"==typeof d.pm_rcpt?d.pm_rcpt:null;if(!msgId||!rcpt)return(0,events_1.emitNotificationEvent)("foregroundWillDisplay",d),false;const primeiraVez=!await wasSeen(msgId);try{return await sendReceipt(msgId,rcpt)}catch(e){return console.warn("[PushMesh] falha ao processar recibo",e),false}finally{primeiraVez&&(0,events_1.emitNotificationEvent)("foregroundWillDisplay",d)}}async function handleClick(data){if(!data||"object"!=typeof data)return;const d=data,msgId="string"==typeof d.pm_msg_id?d.pm_msg_id:null,rcpt="string"==typeof d.pm_rcpt?d.pm_rcpt:null;if(msgId&&rcpt&&await sendReceipt(msgId,rcpt,"clique").catch(e=>console.warn("[PushMesh] recibo do clique falhou",e)),(0,events_1.emitNotificationEvent)("click",d),"ios"===react_native_1.Platform.OS&&!temNativoApns()){const url="string"==typeof d.pm_url?d.pm_url.trim():"";url&&abrirQuandoAtivo(url)}}function temNativoApns(){return!!react_native_1.NativeModules.PushMeshApns}function abrirQuandoAtivo(url){const abrir=()=>react_native_1.Linking.openURL(url).catch(e=>console.warn("[PushMesh] não deu para abrir a pm_url do clique",e));if("active"===react_native_1.AppState.currentState)return void abrir();let feito=false;const sub=react_native_1.AppState.addEventListener("change",estado=>{feito||"active"!==estado||(feito=true,sub?.remove?.(),clearTimeout(timer),abrir())}),timer=setTimeout(()=>{feito||(feito=true,sub?.remove?.(),console.warn("[PushMesh] app não ficou ativo a tempo — pm_url não foi aberta"))},1e4)}
@@ -0,0 +1,21 @@
1
+ import type { PushMeshConfig } from './types';
2
+ /**
3
+ * Servidor oficial do PUSHMESH — usado quando o app NÃO passa `baseUrl`.
4
+ *
5
+ * O cliente integra com UMA linha (`PushMesh.init({ appId })`); `baseUrl` fica
6
+ * como override para servidor próprio ou para apontar num servidor local
7
+ * durante o desenvolvimento.
8
+ */
9
+ export declare const BASE_URL_PADRAO = "https://push.pushmesh.io";
10
+ export declare function requireConfig(): PushMeshConfig;
11
+ /** baseUrl sem barra final, para montar rotas sem '//'. */
12
+ export declare function baseUrl(): string;
13
+ /**
14
+ * HTTPS é obrigatório fora de desenvolvimento.
15
+ *
16
+ * Pelo corpo das rotas do SDK trafega o token de push e o external_user_id — que nos
17
+ * apps da casa é o CPF. Em `http://` isso vai em claro para qualquer um no mesmo
18
+ * Wi-Fi. Só liberamos texto puro em __DEV__ e para host local (emulador/simulador),
19
+ * onde o tráfego não sai da máquina.
20
+ */
21
+ export declare function validarBaseUrl(url: string): void;
@@ -0,0 +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};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ "use strict";var __importDefault=this&&this.__importDefault||function(mod){return mod&&mod.__esModule?mod:{default:mod}};Object.defineProperty(exports,"__esModule",{value:true}),exports.KEYS=void 0,exports.getItem=getItem,exports.setItem=setItem,exports.removeItem=removeItem,exports.getJsonSeguro=getJsonSeguro,exports.getJson=getJson,exports.setJson=setJson;const async_storage_1=__importDefault(require("@react-native-async-storage/async-storage"));async function getItem(key){try{return await async_storage_1.default.getItem(key)}catch{return null}}async function setItem(key,value){try{await async_storage_1.default.setItem(key,value)}catch{}}async function removeItem(key){try{await async_storage_1.default.removeItem(key)}catch{}}async function getJsonSeguro(key,fallback){let raw;try{raw=await async_storage_1.default.getItem(key)}catch{return{valor:fallback,falhou:true}}if(!raw)return{valor:fallback,falhou:false};try{return{valor:JSON.parse(raw),falhou:false}}catch{return{valor:fallback,falhou:false}}}async function getJson(key,fallback){return(await getJsonSeguro(key,fallback)).valor}async function setJson(key,value){await setItem(key,JSON.stringify(value))}exports.KEYS={playerId:"pm:player_id",regHash:"pm:reg_hash",externalUserId:"pm:external_user_id",logoutPendente:"pm:logout_pendente",token:"pm:token",transporte:"pm:transporte",firebaseParams:"pm:firebase_params",notificationTypes:"pm:notif_types",receiptLru:"pm:receipt_lru",sessoes:"pm:sessoes",queue:"pm:queue",queueInflight:"pm:queue_inflight",reativaPending:"pm:reativa_pending",tags:"pm:tags",triggers:"pm:triggers",inappPacote:"pm:inapp_pacote",inappEstado:"pm:inapp_estado",inappSessao:"pm:inapp_sessao"};
@@ -0,0 +1,13 @@
1
+ export declare function addTags(tags: Record<string, string>): Promise<void>;
2
+ export declare function addTag(key: string, value: string): Promise<void>;
3
+ export declare function removeTags(keys: string[]): Promise<void>;
4
+ export declare function removeTag(key: string): Promise<void>;
5
+ /** Mapa atual de tags (cache local). */
6
+ export declare function getTags(): Promise<Record<string, string>>;
7
+ /**
8
+ * Sincroniza tags que ficaram só no cache local por não haver player ainda.
9
+ * Chamado logo após um registro bem-sucedido — sem isto, quem chamou addTag antes
10
+ * do init terminar (o caso comum) só via a tag chegar ao servidor na PRÓXIMA vez que
11
+ * mexesse nela; nada no fluxo re-tentava.
12
+ */
13
+ export declare function syncTagsPendentes(): Promise<void>;
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true}),exports.addTags=addTags,exports.addTag=addTag,exports.removeTags=removeTags,exports.removeTag=removeTag,exports.getTags=getTags,exports.syncTagsPendentes=syncTagsPendentes;const queue_1=require("./queue"),state_1=require("./state"),storage_1=require("./storage");async function syncTags(tags){if(!state_1.state.config)return;const playerId=state_1.state.playerId??await(0,storage_1.getItem)(storage_1.KEYS.playerId);playerId&&await(0,queue_1.sendOrQueue)("PUT",`/api/v1/players/${playerId}`,{app_id:(0,state_1.requireConfig)().appId,tags:tags})}async function addTags(tags){const next={...await(0,storage_1.getJson)(storage_1.KEYS.tags,{}),...tags};await(0,storage_1.setJson)(storage_1.KEYS.tags,next),await syncTags(next)}async function addTag(key,value){await addTags({[key]:value})}async function removeTags(keys){const next={...await(0,storage_1.getJson)(storage_1.KEYS.tags,{})};for(const key of keys)delete next[key];await(0,storage_1.setJson)(storage_1.KEYS.tags,next),await syncTags(next)}async function removeTag(key){await removeTags([key])}async function getTags(){return(0,storage_1.getJson)(storage_1.KEYS.tags,{})}async function syncTagsPendentes(){const tags=await(0,storage_1.getJson)(storage_1.KEYS.tags,{});0!==Object.keys(tags).length&&await syncTags(tags)}
@@ -0,0 +1,22 @@
1
+ /** Valor de um gatilho do app: texto, número ou booleano. */
2
+ export type RpValorGatilho = string | number | boolean;
3
+ type Mapa = Record<string, RpValorGatilho>;
4
+ /** Define vários gatilhos de uma vez (mescla com os que já existem). */
5
+ export declare function definirGatilhos(gatilhos: Mapa): Promise<void>;
6
+ /** Define um gatilho do app (ex.: `definirGatilho('tela', 'checkout')`). */
7
+ export declare function definirGatilho(chave: string, valor: RpValorGatilho): Promise<void>;
8
+ /** Remove vários gatilhos do mapa. */
9
+ export declare function removerGatilhos(chaves: string[]): Promise<void>;
10
+ /** Remove um gatilho (ex.: ao sair da tela que o definiu). */
11
+ export declare function removerGatilho(chave: string): Promise<void>;
12
+ /** Esvazia o mapa de gatilhos do app. */
13
+ export declare function limparGatilhos(): Promise<void>;
14
+ /** Mapa atual de gatilhos do app (leitura local). */
15
+ export declare function lerGatilhos(): Promise<Mapa>;
16
+ export declare const addTriggers: typeof definirGatilhos;
17
+ export declare const addTrigger: typeof definirGatilho;
18
+ export declare const removeTriggers: typeof removerGatilhos;
19
+ export declare const removeTrigger: typeof removerGatilho;
20
+ export declare const clearTriggers: typeof limparGatilhos;
21
+ export declare const getTriggers: typeof lerGatilhos;
22
+ export {};
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true}),exports.getTriggers=exports.clearTriggers=exports.removeTrigger=exports.removeTriggers=exports.addTrigger=exports.addTriggers=void 0,exports.definirGatilhos=definirGatilhos,exports.definirGatilho=definirGatilho,exports.removerGatilhos=removerGatilhos,exports.removerGatilho=removerGatilho,exports.limparGatilhos=limparGatilhos,exports.lerGatilhos=lerGatilhos;const lifecycle_1=require("./inapp/lifecycle"),storage_1=require("./storage");async function ler(){return(0,storage_1.getJson)(storage_1.KEYS.triggers,{})}async function gravar(mapa){await(0,storage_1.setJson)(storage_1.KEYS.triggers,mapa),(0,lifecycle_1.aoMudarGatilhos)()}async function definirGatilhos(gatilhos){await gravar({...await ler(),...gatilhos})}async function definirGatilho(chave,valor){await definirGatilhos({[chave]:valor})}async function removerGatilhos(chaves){const mapa={...await ler()};for(const chave of chaves)delete mapa[chave];await gravar(mapa)}async function removerGatilho(chave){await removerGatilhos([chave])}async function limparGatilhos(){await gravar({})}async function lerGatilhos(){return ler()}exports.addTriggers=definirGatilhos,exports.addTrigger=definirGatilho,exports.removeTriggers=removerGatilhos,exports.removeTrigger=removerGatilho,exports.clearTriggers=limparGatilhos,exports.getTriggers=lerGatilhos;
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Tipos públicos do SDK PUSHMESH.
3
+ *
4
+ * Contrato das rotas de SDK:
5
+ * rotas de SDK são públicas (app_id no corpo, sem Authorization),
6
+ * rate-limited por IP no servidor.
7
+ */
8
+ /** device_type do contrato de registro (0 iOS / 1 Android). */
9
+ export declare enum RpDeviceType {
10
+ IOS = 0,
11
+ Android = 1
12
+ }
13
+ /** notification_types do contrato de permissão (SPEC_INAPP_REATIVACAO §1). */
14
+ export declare enum RpNotificationTypes {
15
+ /** ativo / inscrito */
16
+ Ativo = 1,
17
+ /** nunca perguntou / desconhecido (notDetermined) */
18
+ NuncaPerguntou = 0,
19
+ /** bloqueou / desinscreveu (denied) */
20
+ Bloqueado = -2
21
+ }
22
+ export interface PushMeshConfig {
23
+ /** UUID do app no PUSHMESH (obrigatório). */
24
+ appId: string;
25
+ /**
26
+ * Base URL do servidor PUSHMESH. OPCIONAL — sem ela vale o servidor
27
+ * oficial (https://push.pushmesh.io); só o `appId` basta para
28
+ * integrar. Passe uma URL para servidor próprio ou local no desenvolvimento
29
+ * (http:// só é aceito para localhost em build de desenvolvimento).
30
+ */
31
+ baseUrl?: string;
32
+ /**
33
+ * Provedor do token de push do app host (MODO HOST). O SDK não embute o
34
+ * Firebase do host: quem já tem @react-native-firebase/messaging entrega o
35
+ * token por aqui OU chama PushMesh.setToken(token) depois do init.
36
+ *
37
+ * Opcional (MODO ZERO-CONFIG): sem getToken, o SDK obtém o token sozinho —
38
+ * no Android, busca os parâmetros públicos do Firebase no servidor
39
+ * (GET /api/v1/apps/:appId/firebase_params) e inicializa o FirebaseApp do
40
+ * app por código; no iOS, captura o token APNs DIRETO via módulo nativo
41
+ * (sem Firebase; o registro sai com transporte 'apns').
42
+ * getToken SEMPRE vence quando presente (precedência do modo host — nesse
43
+ * caso o transporte segue o default do servidor, 'fcm').
44
+ */
45
+ getToken?: () => Promise<string>;
46
+ /**
47
+ * device_type explícito. Default: 'auto' (0 iOS / 1 Android via Platform).
48
+ */
49
+ deviceType?: 'auto' | RpDeviceType;
50
+ /** Versão do app (ex.: '3.2.1') — vai no registro do player. */
51
+ appVersion?: string;
52
+ /** Modelo do aparelho (ex.: 'SM-A32') — se omitido o SDK tenta Platform.constants. */
53
+ deviceModel?: string;
54
+ /**
55
+ * external_user_id já conhecido no boot (ex.: CPF salvo em sessão).
56
+ * Equivalente a chamar login() logo após o init — inclusive na TROCA de
57
+ * usuário: se diferir do gravado, o init faz o login do novo (o anterior
58
+ * não fica colado no aparelho).
59
+ */
60
+ externalUserId?: string;
61
+ /** Timeout de rede por chamada em ms. Default: 10000. */
62
+ requestTimeoutMs?: number;
63
+ /**
64
+ * Montagem AUTOMÁTICA do host in-app. Default: **false** (opt-in) enquanto a
65
+ * via segura não fica pronta: hoje ela troca o provedor de wrapper do React
66
+ * Native, e um wrapper registrado pelo app ANTES de importar o SDK seria
67
+ * apagado (o RN só tem setter, não devolve o wrapper atual) — SafeArea,
68
+ * Redux, tema sumiriam em silêncio. Com o padrão, monte
69
+ * `<PushMeshInAppHost />` na raiz: é uma linha e é o caminho provado.
70
+ */
71
+ inAppAutoMontar?: boolean;
72
+ }
73
+ /**
74
+ * Ação (discriminada por `tipo`) — contrato In-App V1 §2.1 / V2 §2.5.
75
+ * F1/F2 aceitam exatamente estes três; o SDK confirma `open_url` com
76
+ * `Linking.canOpenURL` antes de abrir (falha fecha limpo, nunca crasha o host).
77
+ */
78
+ export type RpInAppAction = {
79
+ tipo: 'dismiss';
80
+ } | {
81
+ tipo: 'open_url';
82
+ url: string;
83
+ } | {
84
+ tipo: 'open_notification_settings';
85
+ };
86
+ /** Botão de conteúdo — rótulo + ação (contrato §2.1). */
87
+ export interface RpInAppButton {
88
+ texto: string;
89
+ acao: RpInAppAction;
90
+ }
91
+ /** Formatos servidos (contrato V2 §4). */
92
+ export type RpInAppFormato = 'banner_top' | 'banner_bottom' | 'modal' | 'fullscreen';
93
+ export interface RpBlocoTexto {
94
+ tipo: 'texto';
95
+ texto: string;
96
+ fonte?: 'sistema' | 'serifada' | 'monoespacada';
97
+ peso?: 'regular' | 'medio' | 'negrito';
98
+ italico?: boolean;
99
+ sublinhado?: boolean;
100
+ cor?: string;
101
+ tamanho?: number;
102
+ alinhamento?: 'esquerda' | 'centro' | 'direita';
103
+ }
104
+ export interface RpBlocoImagem {
105
+ tipo: 'imagem';
106
+ url: string;
107
+ acao_click?: RpInAppAction | null;
108
+ sangra?: boolean;
109
+ raio?: number;
110
+ /** Respiro por lado em px (0..64, default 0) — contrato V2 §2.2. Campo
111
+ * ausente = tudo 0. Sem este tipo o painel emitiria e o SDK descartaria
112
+ * em silêncio (a classe de bug que zerou o funil na F1). */
113
+ margens?: {
114
+ topo?: number;
115
+ direita?: number;
116
+ baixo?: number;
117
+ esquerda?: number;
118
+ };
119
+ }
120
+ export interface RpBlocoBotao {
121
+ tipo: 'botao';
122
+ texto: string;
123
+ acao: RpInAppAction;
124
+ cor_fundo?: string;
125
+ cor_texto?: string;
126
+ raio?: number;
127
+ /**
128
+ * Tipografia do RÓTULO — os MESMOS nomes e valores do bloco de texto (o
129
+ * renderizador reusa as mesmas funções: uma implementação só). Campo ausente
130
+ * = o desenho de hoje (peso 600, 15 px, fonte do sistema, centralizado).
131
+ * Sem estes campos no tipo, o painel emitiria e o SDK DESCARTARIA em
132
+ * silêncio — a classe de bug que já nos mordeu quatro vezes.
133
+ */
134
+ fonte?: 'sistema' | 'serifada' | 'monoespacada';
135
+ peso?: 'regular' | 'medio' | 'negrito';
136
+ italico?: boolean;
137
+ sublinhado?: boolean;
138
+ tamanho?: number;
139
+ alinhamento?: 'esquerda' | 'centro' | 'direita';
140
+ }
141
+ export interface RpBlocoEspacador {
142
+ tipo: 'espacador';
143
+ altura: number;
144
+ }
145
+ /** União aberta: o `{ tipo: string }` final é o cinto de segurança (V9). */
146
+ export type RpBloco = RpBlocoTexto | RpBlocoImagem | RpBlocoBotao | RpBlocoEspacador | {
147
+ tipo: string;
148
+ };
149
+ /** Chrome da mensagem — superset aditivo do estilo V1 (contrato V2 §3). */
150
+ export interface RpInAppEstilo {
151
+ fundo?: string;
152
+ texto?: string;
153
+ botao_fundo?: string;
154
+ botao_texto?: string;
155
+ raio?: number;
156
+ escurecer?: number;
157
+ padding_px?: number;
158
+ imagem_sangra?: boolean;
159
+ fundo_imagem?: string | null;
160
+ /**
161
+ * X de fechar. `cor` é o glifo e `fundo` é o chip atrás dele; AUSENTES, o
162
+ * renderizador decide por CONTRASTE AUTOMÁTICO (nunca X preto sobre cartão
163
+ * preto, nunca chip escuro sob glifo escuro). Sem `cor`/`fundo` no tipo, o
164
+ * campo do painel seria descartado em silêncio.
165
+ */
166
+ close?: {
167
+ mostrar?: boolean;
168
+ icone?: 'x' | 'x_circulo' | 'seta';
169
+ tamanho?: number;
170
+ cor?: string;
171
+ /**
172
+ * Chip atrás do glifo. AUSENTE e `null` são DIFERENTES (contrato V2 §3.2):
173
+ * ausente = o renderizador decide (chip automático quando há arte atrás);
174
+ * `null` = SEM chip, e vence até o automático — é a mão do operador dizendo
175
+ * "não quero". Sem o `| null` aqui o SDK engolia o `null` como ausente e o
176
+ * chip VOLTAVA depois de tirado (achado do auditor 2026-08-04).
177
+ */
178
+ fundo?: string | null;
179
+ };
180
+ acao_card?: RpInAppAction | null;
181
+ }
182
+ /**
183
+ * `conteudo` resolvido (flat) que vem no pending: OU os campos fixos do V1,
184
+ * OU a pilha `blocos[]` do schema 2 — nunca os dois (contrato V2 §1.1).
185
+ */
186
+ export interface RpInAppConteudo {
187
+ imagem?: string | null;
188
+ titulo?: string | null;
189
+ corpo?: string | null;
190
+ botao?: RpInAppButton | null;
191
+ botao_secundario?: RpInAppButton | null;
192
+ acao_card?: RpInAppAction | null;
193
+ blocos?: RpBloco[];
194
+ }
195
+ /**
196
+ * QUANDO a mensagem aparece (contrato §2.3), sempre decidido NO APARELHO.
197
+ * Forma PLANA: campo que não pertence ao `tipo` vem ausente — e o aparelho
198
+ * ignora o que sobrar, nunca adivinha.
199
+ */
200
+ export interface RpInAppGatilho {
201
+ /** `ao_abrir` · `evento` · `sessao_duracao` · `inatividade` (desconhecido é ignorado). */
202
+ tipo: string;
203
+ /** atraso entre a condição casar e exibir (0..60000). */
204
+ atraso_ms?: number;
205
+ /** SÓ `evento`: chave que o app define com `PushMesh.definirGatilho`. */
206
+ chave?: string;
207
+ /** SÓ `evento`: como comparar. */
208
+ comparador?: 'igual' | 'diferente' | 'maior' | 'menor' | 'existe' | 'nao_existe';
209
+ /** SÓ `evento`: o TIPO deste valor manda na comparação (texto ou número). */
210
+ valor?: string | number;
211
+ /** SÓ `sessao_duracao`: segundos nesta sessão. */
212
+ segundos?: number;
213
+ /** SÓ `inatividade`: horas desde a última abertura do app. */
214
+ horas?: number;
215
+ }
216
+ /** Regras que o APARELHO aplica (device-first) — contrato §2.4/§2.3.6. */
217
+ export interface RpInAppRegras {
218
+ /** exibições no total neste aparelho (0 = ilimitado). */
219
+ cap_total?: number;
220
+ /** respiro entre exibições da mesma campanha (horas). */
221
+ cooldown_h?: number;
222
+ /** auto-dismiss em ms (0 = desligado). */
223
+ fechar_apos_ms?: number;
224
+ /** cronograma: não exibir ANTES deste instante (ISO-8601 UTC; null = já). */
225
+ inicia_em?: string | null;
226
+ expira_em?: string | null;
227
+ prioridade?: number;
228
+ }
229
+ /** Uma mensagem do pacote (GET /api/v1/inapp/pending §2). */
230
+ export interface RpInAppMessage {
231
+ id: string;
232
+ /** pm_iam — o SDK só ECOA no recibo; NUNCA calcula (contrato §3). */
233
+ prova?: string;
234
+ /** schema SERVIDO (1 campos fixos · 2 blocos · 3 + gatilhos novos). */
235
+ schema?: 1 | 2 | 3;
236
+ formato: RpInAppFormato;
237
+ conteudo: RpInAppConteudo;
238
+ estilo?: RpInAppEstilo;
239
+ gatilho?: RpInAppGatilho;
240
+ regras?: RpInAppRegras;
241
+ }
242
+ /**
243
+ * O pacote da sessão (GET /api/v1/inapp/pending §1). O aparelho o guarda com
244
+ * ETag e decide a exibição localmente durante toda a `validade_s`.
245
+ */
246
+ export interface RpInAppPacote {
247
+ pacote_id: string | null;
248
+ messages: RpInAppMessage[];
249
+ validade_s: number;
250
+ cap_global_h: number;
251
+ etag: string | null;
252
+ }
253
+ /**
254
+ * Eventos do funil in-app (contrato §4.1). F1 envia `impressao | clique |
255
+ * fechou`; `reativou`/`imagem_falhou` são reservados da F2 (o servidor ainda
256
+ * não os aceita — o SDK os trata como no-op até lá).
257
+ */
258
+ export type RpInAppEventType = 'impressao' | 'clique' | 'fechou' | 'reativou' | 'imagem_falhou';
@@ -0,0 +1 @@
1
+ "use strict";var RpDeviceType,RpNotificationTypes;Object.defineProperty(exports,"__esModule",{value:true}),exports.RpNotificationTypes=exports.RpDeviceType=void 0,function(RpDeviceType){RpDeviceType[RpDeviceType.IOS=0]="IOS",RpDeviceType[RpDeviceType.Android=1]="Android"}(RpDeviceType||(exports.RpDeviceType=RpDeviceType={})),function(RpNotificationTypes){RpNotificationTypes[RpNotificationTypes.Ativo=1]="Ativo",RpNotificationTypes[RpNotificationTypes.NuncaPerguntou=0]="NuncaPerguntou",RpNotificationTypes[RpNotificationTypes.Bloqueado=-2]="Bloqueado"}(RpNotificationTypes||(exports.RpNotificationTypes=RpNotificationTypes={}));
@@ -0,0 +1,45 @@
1
+ /** Parâmetros públicos do Firebase Android (contrato do servidor). */
2
+ export interface FirebaseParamsAndroid {
3
+ project_id?: string | null;
4
+ sender_id: string;
5
+ app_id: string;
6
+ api_key: string;
7
+ package_name?: string;
8
+ }
9
+ /** Apenas para testes. */
10
+ export declare function __resetZeroConfigForTests(): void;
11
+ /**
12
+ * Marcador do dual-run: a mensagem é NOSSA quando tem chave `pmx_*` (bloco de
13
+ * exibição data-only do servidor). ESPELHO do `isNossa` no Kotlin
14
+ * (PushMeshMessageRouter) — mudou aqui, muda lá. Mensagem sem o marcador é
15
+ * de outra origem (ex.: provedor legado) e passa intocada.
16
+ */
17
+ export declare function mensagemDoPushMesh(data: Record<string, unknown>): boolean;
18
+ /**
19
+ * Boot do modo zero-config Android. Chamado por players.init quando não há
20
+ * getToken. Nunca lança — falhas viram warn que ensina.
21
+ */
22
+ export declare function bootZeroConfigAndroid(): Promise<void>;
23
+ /**
24
+ * Entrega ao nativo Android a config do recibo DIRETO (POST Kotlin mesmo com
25
+ * o JS morto). Chamado por players.register() após cada registro bem-sucedido.
26
+ */
27
+ export declare function sincronizarReciboNativo(playerId: string): Promise<void>;
28
+ /**
29
+ * Boot do modo zero-config iOS: APNs DIRETO (sem Firebase). Chamado por
30
+ * players.init quando não há getToken e a plataforma é iOS.
31
+ * Nunca lança — falhas viram warn que ensina.
32
+ */
33
+ export declare function bootZeroConfigIos(): Promise<void>;
34
+ /**
35
+ * Drena as mensagens recebidas SEM JS ativo (app morto/background), bufferizadas
36
+ * pelo nativo. Chamado por players.init DEPOIS do register() — o recibo de
37
+ * entrega precisa do player_id, que só existe após o registro.
38
+ * No-op quando o módulo nativo está ausente (o boot já avisou).
39
+ * Um item que falha NÃO derruba o resto do buffer.
40
+ */
41
+ export declare function drenarMensagensPendentes(): Promise<void>;
42
+ /** Clique com o app MORTO (Android): extras do launch intent via nativo. */
43
+ export declare function processarCliqueInicial(): Promise<void>;
44
+ /** Clique com o app MORTO (iOS): payload pendente guardado pelo nativo. */
45
+ export declare function processarCliquePendenteIos(): Promise<void>;
@@ -0,0 +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.__resetZeroConfigForTests=__resetZeroConfigForTests,exports.mensagemDoPushMesh=mensagemDoPushMesh,exports.bootZeroConfigAndroid=bootZeroConfigAndroid,exports.sincronizarReciboNativo=sincronizarReciboNativo,exports.bootZeroConfigIos=bootZeroConfigIos,exports.drenarMensagensPendentes=drenarMensagensPendentes,exports.processarCliqueInicial=processarCliqueInicial,exports.processarCliquePendenteIos=processarCliquePendenteIos;const react_native_1=require("react-native"),http_1=require("./http"),receipts_1=require("./receipts"),state_1=require("./state"),storage_1=require("./storage"),ERRO_ANDROID_NAO_CONFIGURADO="[PushMesh] zero-config: app Android não configurado no servidor. Como corrigir: cole o google-services.json no painel (Configurações → FCM) ou passe getToken no init.",ERRO_RESPOSTA_NAO_JSON="[PushMesh] zero-config: firebase_params respondeu 200 SEM JSON (portal cativo/proxy?) — não é o painel. Como corrigir: saia da rede com portal; retenta no próximo boot.",ERRO_MODULO_AUSENTE="[PushMesh] zero-config: módulo nativo PushMeshFirebase ausente (JS puro/Expo Go). Como corrigir: rebuild nativo (npx react-native run-android) ou passe getToken no init.",ERRO_MODULO_APNS_AUSENTE="[PushMesh] zero-config iOS: módulo nativo PushMeshApns ausente (JS puro/Expo Go). Como corrigir: cd ios && pod install + rebuild, ou passe getToken no init.";function moduloNativo(){return react_native_1.NativeModules.PushMeshFirebase}function moduloApns(){return react_native_1.NativeModules.PushMeshApns}let eventosConectados=false,eventosApnsConectados=false;function __resetZeroConfigForTests(){eventosConectados=false,eventosApnsConectados=false}function mensagemDoPushMesh(data){return Object.keys(data).some(k=>k.startsWith("pmx_"))}async function bootZeroConfigAndroid(){const nativo=moduloNativo();if(!nativo)return void console.warn(ERRO_MODULO_AUSENTE);conectarEventos();const cached=await lerParamsCache();if(cached)return await configureNativo(nativo,cached),await sincronizarToken(nativo),void revalidarParamsEmBackground(nativo,cached);const fresco=await buscarFirebaseParams();fresco&&(await(0,storage_1.setItem)(storage_1.KEYS.firebaseParams,JSON.stringify(fresco)),await configureNativo(nativo,fresco),await sincronizarToken(nativo))}async function lerParamsCache(){const raw=await(0,storage_1.getItem)(storage_1.KEYS.firebaseParams);if(!raw)return null;try{const parsed=JSON.parse(raw);return parsed&&"string"==typeof parsed.app_id?parsed:null}catch{return null}}async function buscarFirebaseParams(){let res;try{res=await(0,http_1.get)(`/api/v1/apps/${(0,state_1.requireConfig)().appId}/firebase_params`)}catch(e){return console.warn("[PushMesh] zero-config: rede falhou em firebase_params — retenta no boot.",e),null}if(!res.ok)return console.warn(`[PushMesh] zero-config: firebase_params respondeu ${res.status}.`),null;if(!res.json||"object"!=typeof res.json)return console.warn(ERRO_RESPOSTA_NAO_JSON),null;return res.json.android||(console.warn(ERRO_ANDROID_NAO_CONFIGURADO),null)}async function configureNativo(nativo,params){try{await nativo.configure({app_id:params.app_id,api_key:params.api_key,sender_id:params.sender_id,project_id:params.project_id??null})}catch(e){console.warn("[PushMesh] zero-config: configure nativo falhou — confira o painel (Configurações → FCM) ou passe getToken no init.",e)}}async function sincronizarToken(nativo,viaRevalidacao=false){let token=null;try{token=await nativo.getToken()}catch(e){console.warn("[PushMesh] zero-config: getToken nativo falhou — tentando token pendente",e)}if(token||(token=await nativo.getPendingToken().catch(()=>null)),await nativo.clearPendingToken().catch(()=>{}),token&&token!==state_1.state.token){if(viaRevalidacao){const{setToken:setToken}=await Promise.resolve().then(()=>__importStar(require("./players")));return void await setToken(token)}state_1.state.token=token,await(0,storage_1.setItem)(storage_1.KEYS.token,token)}}function revalidarParamsEmBackground(nativo,cached){(async()=>{const fresco=await buscarFirebaseParams();fresco&&JSON.stringify(fresco)!==JSON.stringify(cached)&&(await(0,storage_1.setItem)(storage_1.KEYS.firebaseParams,JSON.stringify(fresco)),await configureNativo(nativo,fresco),await sincronizarToken(nativo,true))})().catch(e=>console.warn("[PushMesh] revalidação de firebase_params falhou",e))}async function sincronizarReciboNativo(playerId){if("android"!==react_native_1.Platform.OS)return;const nativo=moduloNativo();nativo&&state_1.state.config&&await nativo.setReceiptConfig({base_url:(0,state_1.baseUrl)(),app_id:state_1.state.config.appId,player_id:playerId})}async function bootZeroConfigIos(){const nativo=moduloApns();if(nativo){conectarEventosApns(nativo),"apns"!==state_1.state.transporte&&(state_1.state.transporte="apns",await(0,storage_1.setItem)(storage_1.KEYS.transporte,"apns"));try{const token=await nativo.getToken();token&&token!==state_1.state.token&&(state_1.state.token=token,await(0,storage_1.setItem)(storage_1.KEYS.token,token))}catch(e){console.warn("[PushMesh] zero-config iOS: falha ao obter o token APNs. Como corrigir: capability Push Notifications no Xcode (device físico) ou getToken no init.",e)}}else console.warn(ERRO_MODULO_APNS_AUSENTE)}function conectarEventosApns(nativo){if(eventosApnsConectados)return;eventosApnsConectados=true;const emitter=new react_native_1.NativeEventEmitter(nativo);emitter.addListener("PushMeshApnsToken",token=>{"string"==typeof token&&token&&Promise.resolve().then(()=>__importStar(require("./players"))).then(m=>m.setToken(token)).catch(e=>console.warn("[PushMesh] falha no re-registro pós-token-apns",e))}),emitter.addListener("PushMeshApnsMessage",userInfo=>{(0,receipts_1.handlePushData)(extrairData(userInfo)).catch(e=>console.warn("[PushMesh] falha ao processar PushMeshApnsMessage",e))}),emitter.addListener("PushMeshApnsClick",userInfo=>{(0,receipts_1.handleClick)(extrairData(userInfo)).catch(e=>console.warn("[PushMesh] falha ao processar PushMeshApnsClick",e))})}async function drenarMensagensPendentes(){const nativo=moduloNativo();if(!nativo)return;const pendentes=await nativo.getPendingMessages();for(const data of pendentes)try{await processarMensagemNativa(data)}catch(e){console.warn("[PushMesh] item do buffer falhou — processando o resto",e)}}async function processarCliqueInicial(){const nativo=moduloNativo();if(!nativo)return;const data=await nativo.getInitialNotification();data&&await(0,receipts_1.handleClick)(data)}async function processarCliquePendenteIos(){const nativo=moduloApns();if(!nativo)return;const data=await nativo.getPendingClick();data&&await(0,receipts_1.handleClick)(data)}function conectarEventos(){eventosConectados||(eventosConectados=true,react_native_1.DeviceEventEmitter.addListener("PushMeshMessage",payload=>{processarMensagemNativa(extrairData(payload)).catch(e=>console.warn("[PushMesh] falha ao processar PushMeshMessage",e))}),react_native_1.DeviceEventEmitter.addListener("PushMeshTokenRefresh",token=>{"string"==typeof token&&token&&Promise.resolve().then(()=>__importStar(require("./players"))).then(m=>m.setToken(token)).catch(e=>console.warn("[PushMesh] falha no re-registro pós-token-refresh",e))}),react_native_1.DeviceEventEmitter.addListener("PushMeshNotificationClick",payload=>{(0,receipts_1.handleClick)(extrairData(payload)).catch(e=>console.warn("[PushMesh] falha ao processar PushMeshNotificationClick",e))}))}function extrairData(payload){if(!payload||"object"!=typeof payload)return{};const p=payload;return p.data&&"object"==typeof p.data?p.data:p}async function processarMensagemNativa(data){await(0,receipts_1.handlePushData)(data)}
@@ -0,0 +1,105 @@
1
+ # Imagem no push do iOS — Notification Service Extension
2
+
3
+ **Tempo: ~5 minutos, uma vez por app.** Depois disso, toda mensagem com
4
+ imagem passa a mostrar a imagem no iPhone, sem mexer em mais nada.
5
+
6
+ ## Por que existe um passo manual aqui
7
+
8
+ O SDK é "só o App ID basta" — e continua sendo em tudo o mais. Este passo é o
9
+ único que a **Apple impõe** e que nenhum SDK consegue eliminar:
10
+
11
+ - o servidor já manda tudo (`mutable-content: 1` e a URL da imagem no
12
+ payload). Essa parte está pronta e não depende de você;
13
+ - no iOS, quem **baixa** a imagem é um processo separado do app, chamado
14
+ *Notification Service Extension*. Esse processo **só existe se o projeto
15
+ tiver um alvo (target) desse tipo**;
16
+ - sem esse alvo, o iPhone ignora o `mutable-content` **em silêncio**: a
17
+ notificação chega certinha, só que sem imagem. É exatamente o sintoma "no
18
+ Android a imagem aparece, no iOS não".
19
+
20
+ Nenhum código dentro do app resolve: com o app em segundo plano ou fechado, o
21
+ app nem roda quando o push chega. No Android nada disso é preciso — lá o
22
+ próprio SDK baixa e exibe.
23
+
24
+ ## Passo a passo
25
+
26
+ 1. Abra o projeto no Xcode (`ios/SeuApp.xcworkspace`).
27
+ 2. **File → New → Target… → Notification Service Extension → Next**.
28
+ 3. **Product Name**: `PushMeshNotificationService` (o nome é livre).
29
+ Confira **Team** (o mesmo do app) e **Language: Swift** → **Finish**.
30
+ 4. O Xcode pergunta se quer ativar o novo *scheme*: pode clicar em **Cancel**
31
+ — assim o esquema do app continua selecionado para rodar.
32
+ 5. O Xcode criou uma pasta com um `NotificationService.swift` de exemplo.
33
+ Substitua o conteúdo dele pelo nosso, com **um comando** na raiz do app:
34
+
35
+ ```sh
36
+ cp node_modules/@pushmesh/sdk/ios/NotificationService/PushMeshNotificationService.swift \
37
+ ios/PushMeshNotificationService/NotificationService.swift
38
+ ```
39
+
40
+ (ajuste o caminho de destino se você deu outro nome ao alvo)
41
+ 6. Rode o app em **aparelho real**. Pronto.
42
+
43
+ Não é preciso `pod install`, não é preciso mexer no Podfile e não é preciso
44
+ tocar no `AppDelegate`: a extensão não usa nenhuma dependência — só
45
+ `Foundation` e `UserNotifications`, que já vêm no sistema.
46
+
47
+ ## Detalhes que custam uma tarde se passarem batido
48
+
49
+ | Ponto | O que fazer |
50
+ |---|---|
51
+ | **Minimum Deployments** | Ponha no alvo da extensão o **mesmo** valor do app (ex.: iOS 15.1). O Xcode costuma criar o alvo com o iOS mais recente — e aí, em todo aparelho com versão anterior, a extensão simplesmente **não roda** (sem erro, sem aviso: a imagem só não aparece). |
52
+ | **Bundle Identifier** | Tem de começar com o do app: `com.suaempresa.app` → `com.suaempresa.app.PushMeshNotificationService`. O Xcode acerta sozinho ao criar; mas se você **renomear o bundle do app depois**, o da extensão **não** é renomeado junto e a build passa a falhar na assinatura. |
53
+ | **Team / assinatura** | Mesmo Team do app. Com assinatura **automática**, o Xcode cria o perfil da extensão sozinho. Com assinatura **manual**, você precisa de um **segundo App ID + perfil de provisionamento** (o do bundle da extensão) no portal da Apple — é o esquecimento clássico de quem assina na mão. |
54
+ | **NSExtensionPrincipalClass** | **Não mexa.** O Info.plist gerado aponta para `$(PRODUCT_MODULE_NAME).NotificationService`, e a classe do nosso arquivo se chama `NotificationService` justamente para casar. Renomear a classe sem renomear lá faz a extensão não rodar **em silêncio**. |
55
+ | **Se preferir arrastar o arquivo** (em vez do `cp`) | Apague antes o `NotificationService.swift` gerado — duas classes com o mesmo nome não compilam. Ao arrastar, **desmarque "Copy items if needed"** e marque em *Target Membership* **só o alvo da extensão**, nunca o app. Vantagem: `npm update` atualiza o arquivo sozinho. Desvantagem: o build passa a depender do `node_modules`. |
56
+
57
+ ### O que **não** é necessário (não perca tempo)
58
+
59
+ - **App Groups**: não. Só seriam precisos para a extensão trocar dados com o
60
+ app — ela não troca; baixa a imagem e entrega.
61
+ - **Keychain Sharing**, **Background Modes** ou **Push Notifications** *no
62
+ alvo da extensão*: não. A capability **Push Notifications** fica no alvo do
63
+ **app**, como já estava.
64
+ - **CocoaPods / Podfile**: não. A extensão não tem dependência nenhuma.
65
+ - Mexer no painel: não. Basta preencher a imagem na mensagem — o servidor já
66
+ manda a flag que acorda a extensão quando há mídia.
67
+
68
+ ## Limites que são da Apple (e o que a extensão faz com eles)
69
+
70
+ - **Formatos aceitos**: `jpg`, `png`, `gif`. `webp`, `heic` e `avif` são
71
+ recusados pelo iOS — use jpg/png/gif na CDN.
72
+ - **Tamanho**: até 10 MB. Na prática, imagem de push boa é **abaixo de 1 MB**
73
+ (o aparelho baixa na hora de exibir, muitas vezes em rede ruim).
74
+ - **HTTPS**: obrigatório. URL `http://` é bloqueada pelo App Transport
75
+ Security e o download nem sai.
76
+ - **Prazo**: a Apple dá ~30s à extensão. A nossa usa 10s por tentativa e
77
+ 20s no total; estourou, entrega a notificação **sem** a imagem.
78
+ - **Como aparece**: miniatura à direita no banner; a imagem grande aparece
79
+ quando a notificação é **expandida** (puxar para baixo / manter pressionado).
80
+
81
+ Em qualquer falha — 404, formato recusado, CDN fora do ar, prazo estourado —
82
+ **a notificação chega inteira, só sem imagem**. Isso é lei do arquivo e está
83
+ provado por teste executável na suíte do SDK (`__tests__/ios-nse.test.ts`).
84
+
85
+ ## Como saber se está funcionando
86
+
87
+ Com o iPhone conectado ao Mac, abra o **Console.app**, selecione o aparelho e
88
+ filtre por `[PushMesh NSE]`. Cada desistência é logada com a **causa** e o
89
+ **como corrigir** (URL inválida, 404, formato não aceito, arquivo grande
90
+ demais, prazo estourado).
91
+
92
+ Se **nenhuma** linha `[PushMesh NSE]` aparece quando chega um push com
93
+ imagem, a extensão não está rodando — reveja o *Minimum Deployments* e o
94
+ *Bundle Identifier* da tabela acima.
95
+
96
+ > Teste em **aparelho real**. No simulador o comportamento das extensões de
97
+ > notificação varia conforme a versão do Xcode: um push sem imagem lá não
98
+ > prova nada.
99
+
100
+ ## O que esta extensão ainda não faz
101
+
102
+ **Recibo de entrega em segundo plano/app fechado.** Hoje o iOS só contabiliza
103
+ o que o app vê (app aberto e toque na notificação); no Android o recibo já é
104
+ real nos três estados. É a próxima frente desta mesma extensão — quando
105
+ chegar, o passo de instalação continuará sendo este mesmo, sem retrabalho.