API da ponte JavaScript
Relatórios de falhas e integridade do dispositivo
Erros que a página registra, a última falha nativa e se o celular parece ter root ou estar modificado, com um token opcional do Play Integrity para o seu servidor.
AppMint.integrity helper#
AppMint.integrity.check() / AppMint.integrity.token(nonce, cloudProjectNumber?)
Exemplo
Versões com Promise de getDeviceIntegrity e requestIntegrityToken, presentes antes do primeiro script da sua página quando o build ativou as Verificações de integridade do dispositivo (etapa Integrar, Etapa 3).
Retorna: check() resolve com o objeto das heurísticas ({rooted, emulator, debuggerAttached, signals, installer, installedFromPlay, signatureSha256, method: 'heuristic'}); token(nonce) resolve com a string do token do Play Integrity, ou rejeita com um Error cuja mensagem é o nome do erro (not_enabled, nonce_too_short, network_error, …) e code o código de erro do Play. As heurísticas são indícios; só a decodificação do token pelo seu servidor é um veredito.
async function guard() {
if (!(window.AppMint && AppMint.integrity)) return; // browser, or option off
const local = await AppMint.integrity.check();
if (local.rooted) console.warn('rooted signals:', local.signals);
const { nonce } = await fetch('/api/integrity/nonce').then(r => r.json());
try {
const token = await AppMint.integrity.token(nonce); // project number from the build
await fetch('/api/integrity/verify', { method: 'POST', body: token });
} catch (e) {
console.warn('Play Integrity unavailable:', e.message, e.code);
}
}Avise quando o app for uma cópia reempacotada:
if (window.AppMint && AppMint.integrity) {
const d = await AppMint.integrity.check();
if (!d.installedFromPlay) console.log('installed by', d.installer || 'side-loaded');
if (d.signatureSha256 && d.signatureSha256 !== MY_CERT_SHA256) reportTamper(d.signatureSha256);
}Trate uma falha de token pelo nome:
async function getToken(nonce) {
if (!(window.AppMint && AppMint.integrity)) return null;
try {
return await AppMint.integrity.token(nonce);
} catch (e) {
if (e.message === 'not_enabled') console.warn('Device integrity checks are off in this build');
else if (e.message === 'nonce_too_short') console.warn('the server nonce must be longer');
else console.warn('Play Integrity error', e.message, e.code);
return null;
}
}getDeviceIntegrity ponte#
window.WebToApk.getDeviceIntegrity(): String
HEURÍSTICAS locais de root / emulador / depurador (veja DeviceIntegrity) como JSON: `{rooted, emulator, debuggerAttached, signals:[…], installer, installedFromPlay, signatureSha256, method:"heuristic", sdk}`. Um resultado limpo significa "nenhum sinal encontrado", não prova - para um veredito confiável, envie um token do Play Integrity ao seu servidor (requestIntegrityToken). `{"error":"not_enabled"}` a menos que o build tenha ativado a Integridade do dispositivo. Bloqueia por alguns milissegundos (lê arquivos): chame uma vez.
Exemplo
HEURÍSTICAS locais de root, emulador e depurador como string JSON. Um resultado limpo significa "nenhum sinal encontrado", não prova: um usuário determinado com um módulo que esconde o root passa. Use para avisar ou para adicionar uma verificação; para um veredito confiável, envie um token do Play Integrity ao seu servidor (requestIntegrityToken).
Retorna: '{"rooted":false,"emulator":false,"debuggerAttached":false,"signals":[],"installer":"com.android.vending","installedFromPlay":true,"signatureSha256":"AB:CD:…","method":"heuristic","sdk":34}', ou '{"error":"not_enabled"}'. Os sinais indicam o que foi encontrado: test-keys, su-binary:/system/xbin/su, root-path:/data/adb/magisk, root-app:com.topjohnwu.magisk, system-writable, ro.debuggable=1, ro.secure=0, selinux-permissive, bootloader-unlocked, emulator:*, debugger-attached. Requer: ative as Verificações de integridade do dispositivo na etapa Integrar (Etapa 3). Leva alguns milissegundos (lê arquivos): chame uma vez, não em um laço.
function deviceCheck() {
if (!(window.WebToApk && window.WebToApk.getDeviceIntegrity)) return null;
const r = JSON.parse(WebToApk.getDeviceIntegrity());
if (r.error) return null; // 'not_enabled'
return r;
}
const d = deviceCheck();
if (d && d.rooted) {
showNotice('This phone looks rooted (' + d.signals.join(', ') + '). Some features are turned off.');
}
if (d && !d.installedFromPlay) console.log('installed by', d.installer || 'unknown (side-loaded)');Verifique sua própria assinatura - uma cópia reempacotada do seu app é assinada com outra chave:
const MY_CERT = 'AB:CD:EF:…'; // Play Console → App integrity → App signing key SHA-256
const d = deviceCheck();
if (d && d.signatureSha256 && d.signatureSha256 !== MY_CERT) reportTamper();Peça a um servidor um veredito confiável com um token do Play Integrity (a resposta chega como um evento).
window.addEventListener('appmint:integrity', function (e) {
if (e.detail.error) { showNotice('Check failed: ' + e.detail.error); return; }
fetch('https://api.example.com/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: e.detail.token })
});
});
if (window.WebToApk && WebToApk.requestIntegrityToken) {
const nonce = crypto.randomUUID().replace(/-/g, '') + '0000'; // your server issues and checks this
WebToApk.requestIntegrityToken(nonce, '123456789012'); // your Google Cloud project number
}getLastCrash ponte#
window.WebToApk.getLastCrash(): String
A falha mais recente que esta instalação registrou (o app fechando por um erro não capturado, ou o renderizador da página travando), como JSON `{kind, message, stack, url, at, count, appVersion, user, fp}`, ou "" quando não houve nenhuma (ou os Relatórios de falhas estão desligados). Mantida após o envio, para a página poder dizer "desculpe, o app fechou da última vez".
Exemplo
A falha mais recente que esta instalação registrou - o app fechando por um erro não capturado (kind: "native") ou o renderizador da página travando (kind: "renderer") - como string JSON, ou '' quando não houve nenhuma.
Retorna: '{"kind","message","stack","url","at","count","appVersion","user","fp"}' ou ''. Também '' quando o build não ativou os Relatórios de falhas. Requer: Relatórios de falhas ligados na etapa Integrar (Etapa 3).
Peça desculpas uma vez depois de uma falha:
(function () {
if (!(window.WebToApk && window.WebToApk.getLastCrash)) return;
const raw = WebToApk.getLastCrash();
if (!raw) return;
const crash = JSON.parse(raw);
if (localStorage.getItem('seenCrash') === String(crash.at)) return; // shown already
localStorage.setItem('seenCrash', String(crash.at));
showBanner('Sorry, the app closed unexpectedly last time. We have been told about it.');
})();Envie a falha armazenada ao seu próprio servidor uma vez, com a versão do app em que ela ocorreu.
(function () {
if (!(window.WebToApk && WebToApk.getLastCrash)) return;
const raw = WebToApk.getLastCrash();
if (!raw) return; // no crash, or Crash reports are off
const c = JSON.parse(raw);
if (localStorage.getItem('sentCrash') === String(c.at)) return;
fetch('https://api.example.com/crashes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ kind: c.kind, message: c.message, version: c.appVersion, count: c.count })
}).then(function () { localStorage.setItem('sentCrash', String(c.at)); });
})();Trate cada tipo para dar uma mensagem adequada.
const raw = window.WebToApk && WebToApk.getLastCrash && WebToApk.getLastCrash();
if (raw) {
const c = JSON.parse(raw);
showBanner(c.kind === 'renderer'
? 'The page ran out of memory last time. Try closing other apps.'
: 'The app closed unexpectedly last time.');
}Observações: ela permanece após o envio do relatório, então compare at com o que você guardou para mostrar a mensagem uma vez. Erros da página e chamadas de logError não são falhas e nunca aparecem aqui.
logError ponte#
window.WebToApk.logError(message: String, stack: String): Boolean
Relatórios de falhas: registra um erro que sua página CAPTUROU (uma chamada de pagamento que falhou, um erro de leitura) para que apareça na tela de Relatórios de falhas do AppMint junto das falhas, agrupado e contado. [stack]: `err.stack` ou "". Retorna false quando o build não ativou os Relatórios de falhas, quando ambos estão vazios, ou quando o mesmo erro já foi registrado muitas vezes nesta sessão. Nunca coloque dados pessoais (e-mail, telefone, tokens) na mensagem.
Exemplo
Registra um erro que sua página CAPTUROU (uma chamada de pagamento que falhou, um erro de leitura de JSON) para que apareça na tela Relatórios de falhas do AppMint (Atualizar o app → seu app → Relatórios de falhas), agrupado com as falhas e contado. Erros não capturados e promises rejeitadas sem tratamento são registrados por você; isto é para os que você tratou.
Retorna: true quando registrado; false quando o build não ativou os Relatórios de falhas, quando ambos os argumentos estão vazios, ou quando o mesmo erro já foi registrado muitas vezes nesta sessão. Os relatórios são enviados na próxima vez que o app abrir. Requer: ative os Relatórios de falhas na etapa Integrar (Etapa 3) ao compilar. Nunca coloque dados pessoais (e-mail, telefone, tokens) na mensagem.
Registre um erro tratado:
function logError(err, context) {
if (!(window.WebToApk && window.WebToApk.logError)) return false; // browser, or an older build
var msg = (context ? context + ': ' : '') + (err && err.message ? err.message : String(err));
return WebToApk.logError(msg, (err && err.stack) || '');
}
async function checkout(cart) {
try {
await payments.charge(cart.total);
} catch (err) {
logError(err, 'checkout'); // shows up as "Logged by your page"
showToast('Payment failed, please try again');
}
}Registre chamadas de API que falharam sem a query string do endereço (ela pode conter tokens):
async function api(path) {
const res = await fetch('/api' + path);
if (!res.ok && window.WebToApk && WebToApk.logError) {
WebToApk.logError('API ' + res.status + ' ' + path.split('?')[0], '');
}
return res;
}Registre um erro capturado com um id de usuário, para que os problemas de um usuário possam ser encontrados depois.
if (window.WebToApk && WebToApk.setCrashUserId) WebToApk.setCrashUserId('user_' + currentUser.id);
try {
JSON.parse(savedState);
} catch (err) {
if (window.WebToApk && WebToApk.logError) {
const logged = WebToApk.logError('state parse: ' + err.message, err.stack || '');
if (!logged) console.warn('not recorded (Crash reports off, or repeated too often)');
}
}Observações: relatórios com a mesma causa são agrupados mesmo quando os números na mensagem diferem (item 42 e item 7). O endereço da página é armazenado sem a query e o fragmento.
requestIntegrityToken ponte#
window.WebToApk.requestIntegrityToken(nonce: String, cloudProjectNumber: String): String
Pede ao Google Play um token do Play Integrity (requisição clássica). [nonce]: Base64 seguro para URL, de 16 a 500 caracteres, gerado pelo SEU servidor para esta única requisição. [cloudProjectNumber]: o número do seu projeto do Google Cloud, ou "" para usar o definido no build (um app instalado pelo Play pode deixar os dois vazios). Retorna um id de requisição na hora ("" quando o build não ativou a Integridade do dispositivo); a resposta chega como `appmint:integrity` / `window.onAppMintIntegrity({requestId, token})` ou `{requestId, error, code}`. Envie o token ao seu servidor, que o decodifica com o Google (playintegrity.googleapis.com … :decodeIntegrityToken) e confere o nonce.
Exemplo
Pede ao Google Play um token do Play Integrity (requisição clássica). O token é opaco no celular: o SEU SERVIDOR o envia ao Google, lê o veredito (app genuíno? dispositivo genuíno? licenciado?) e confere se o nonce é o que ele emitiu. Essa verificação no servidor é o que torna a resposta confiável.
Retorna: um id de requisição na hora ('' quando o build não ativou a Integridade do dispositivo). A resposta chega como o evento appmint:integrity (e window.onAppMintIntegrity) com {requestId, token} ou {requestId, error, code} - erros como nonce_too_short, nonce_is_not_base64, network_error, play_store_not_found, cloud_project_number_is_invalid, too_many_requests. Requer: Verificações de integridade do dispositivo ligadas na etapa Integrar (Etapa 3), a API Play Integrity ativada para o app (Play Console → Integridade do app) e - para um app NÃO instalado pelo Google Play - o número do seu projeto do Google Cloud, no assistente ou como segundo argumento ('' usa o do build).
async function integrityToken() {
// 1. Your server makes a one-time nonce: URL-safe Base64, 16-500 characters.
const { nonce } = await fetch('/api/integrity/nonce').then(r => r.json());
return new Promise((resolve, reject) => {
const id = WebToApk.requestIntegrityToken(nonce, '');
if (!id) return reject(new Error('not_enabled'));
window.addEventListener('appmint:integrity', function on(e) {
if (e.detail.requestId !== id) return;
window.removeEventListener('appmint:integrity', on);
e.detail.token ? resolve(e.detail.token) : reject(new Error(e.detail.error));
});
});
}
// 2. Your server decodes it with Google and decides.
const token = await integrityToken();
const verdict = await fetch('/api/integrity/verify', { method: 'POST', body: token }).then(r => r.json());A etapa do servidor - chame POST https://playintegrity.googleapis.com/v1/<your.package>:decodeIntegrityToken com {"integrity_token": token} usando uma conta de serviço do projeto Cloud vinculado e depois verifique:
const p = decoded.tokenPayloadExternal;
const ok = p.requestDetails.nonce === issuedNonce &&
p.appIntegrity.appRecognitionVerdict === 'PLAY_RECOGNIZED' &&
p.deviceIntegrity.deviceRecognitionVerdict.includes('MEETS_DEVICE_INTEGRITY');A forma com callback, para código que não consegue escutar eventos:
window.onAppMintIntegrity = function (d) {
if (d.error) console.warn('integrity failed', d.error, d.code);
else sendToServer(d.token);
};
WebToApk.requestIntegrityToken(nonceFromServer, '123456789012');setCrashUserId ponte#
window.WebToApk.setCrashUserId(id: String): Boolean
Relatórios de falhas: SEU id do usuário conectado (letras, dígitos e _ @ . : + -, até 128), anexado aos relatórios a partir de agora para você contar usuários afetados e encontrar as falhas de um usuário. "" o limpa. Nunca um e-mail ou número de telefone. Retorna false para um id com outros caracteres, ou quando os Relatórios de falhas estão desligados.
Exemplo
Anexa o SEU id do usuário conectado aos relatórios de falhas a partir de agora, para a tela de Relatórios de falhas poder contar quantos usuários um problema afeta. Chame depois do login; chame com '' ao sair.
Retorna: true quando salvo; false para um id com caracteres diferentes de letras, dígitos e _ @ . : + - (ou maior que 128), ou quando o build não ativou os Relatórios de falhas. Requer: Relatórios de falhas ligados na etapa Integrar (Etapa 3). Use o id do seu banco de dados, nunca um e-mail ou número de telefone.
function crashUser(id) {
if (!(window.WebToApk && window.WebToApk.setCrashUserId)) return false;
return WebToApk.setCrashUserId(id);
}
async function signIn(email, password) {
const user = await myBackend.signIn(email, password);
crashUser(user.id); // e.g. 'u_42'
}
function signOut() {
crashUser(''); // later reports carry no user
}Um id recusado, ou um build sem Relatórios de falhas, retorna false:
function setCrashUser(id) {
if (!(window.WebToApk && WebToApk.setCrashUserId)) return;
if (!WebToApk.setCrashUserId(id)) {
console.warn('Crash user id not saved: bad characters, over 128 long, or Crash reports are off');
}
}
setCrashUser('u_42'); // your database id, never an e-mail addressAvise o usuário sobre a última falha depois de reiniciar:
if (window.WebToApk && WebToApk.getLastCrash) {
const raw = WebToApk.getLastCrash(); // '' when there has been none
if (raw) {
const c = JSON.parse(raw);
showMessage('Sorry, the app closed last time (' + c.kind + ').');
}
}Observações: o id fica no celular até você mudá-lo e só é enviado dentro dos relatórios de falhas.