API del puente de JavaScript
Trabajos en segundo plano
Trabajo que sigue en marcha después de cerrar la app: un autoguardado duradero, una sincronización periódica.
workCancel puente#
window.WebToApk.workCancel(id: String)
No necesita permisos; los resultados se devuelven como `appmint:work` / window.onAppMintWork.
Se describe por su grupo, Trabajos en segundo plano (WorkManager): autoguardado duradero + sincronización nativa periódica, y no por separado.
Ejemplo
Cancela un trabajo en segundo plano (único o repetitivo) por su id y borra su solicitud guardada.
Devuelve: nada directamente. El evento appmint:work (y window.onAppMintWork) responde { id, status: 'cancelled' }. Requiere: nada.
function stopSync() {
if (!(window.WebToApk && window.WebToApk.workCancel)) return;
WebToApk.workCancel('sync');
}
window.addEventListener('appmint:work', function (e) {
if (e.detail.id === 'sync' && e.detail.status === 'cancelled') {
document.getElementById('sync-status').textContent = 'Background sync is off';
}
});Descarta un borrador: cancela su autoguardado que aún no se ha enviado y luego limpia el editor cuando Android confirme:
function discardDraft() {
if (!(window.WebToApk && window.WebToApk.workCancel)) { clearEditor(); return; }
function onWork(e) {
if (e.detail.id !== 'autosave' || e.detail.status !== 'cancelled') return;
window.removeEventListener('appmint:work', onWork);
clearEditor();
}
window.addEventListener('appmint:work', onWork);
WebToApk.workCancel('autosave');
}Al cerrar sesión, cancela todos los trabajos que tiene esta app, usando los ids que informa workList:
function cancelAllJobs() {
if (!(window.WebToApk && window.WebToApk.workList && window.WebToApk.workCancel)) return;
const requestId = 'signout-' + Date.now();
function onWork(e) {
if (String(e.detail.requestId) !== requestId) return;
window.removeEventListener('appmint:work', onWork);
(e.detail.jobs || []).forEach(function (job) {
WebToApk.workCancel(job.id); // each answers { id, status: 'cancelled' }
});
}
window.addEventListener('appmint:work', onWork);
WebToApk.workList(requestId);
}Notas: responde cancelled incluso cuando ningún trabajo tenía ese id. Un autoguardado que aún no se ha enviado se descarta, así que cancela solo cuando realmente no quieras que se entregue.
workEnqueueOnline puente#
window.WebToApk.workEnqueueOnline(id: String, url: String, method: String, headersJson: String, payload: String)
No necesita permisos; los resultados se devuelven como `appmint:work` / window.onAppMintWork.
Se describe por su grupo, Trabajos en segundo plano (WorkManager): autoguardado duradero + sincronización nativa periódica, y no por separado.
Ejemplo
Entrega a Android una solicitud HTTP para enviarla cuando el teléfono esté conectado, incluso si el usuario cierra la app o reinicia el teléfono. Úsalo para un autoguardado fiable.
Devuelve: nada directamente. La respuesta llega como el evento appmint:work (y window.onAppMintWork(detail) si lo defines): { id, status: 'enqueued' }, o { id, status: 'failed', reason } con motivo invalid_arguments (id vacío o una URL que no empieza con http) o enqueue_failed. Requiere: nada.
Autoguardado duradero: el mismo id reemplaza un guardado que aún no se ha enviado, así que gana el estado más reciente.
function autoSave(state, token) {
if (!(window.WebToApk && window.WebToApk.workEnqueueOnline)) {
showMessage('Offline auto-save works in the installed app only.');
return;
}
WebToApk.workEnqueueOnline(
'autosave', // your job id
'https://api.example.com/save', // http(s) URL
'POST', // POST | PUT | GET ...
JSON.stringify({ Authorization: 'Bearer ' + token }), // headers as a JSON string
JSON.stringify(state) // body as a string ('' for none)
);
}
window.addEventListener('appmint:work', function (e) {
const d = e.detail;
if (d.id !== 'autosave') return;
if (d.status === 'enqueued') showSavedBadge('Will sync when online');
if (d.status === 'failed') showMessage('Could not queue the save: ' + d.reason);
});Pon en cola cada envío de formulario como su propio trabajo (un id único por envío, para que un segundo no reemplace al primero) y espera la respuesta de Android:
function queueFeedback(form) {
return new Promise(function (resolve, reject) {
if (!(window.WebToApk && window.WebToApk.workEnqueueOnline)) {
reject(new Error('not_in_app'));
return;
}
const id = 'feedback-' + Date.now();
function onWork(e) {
if (e.detail.id !== id) return; // another job's answer
window.removeEventListener('appmint:work', onWork);
if (e.detail.status === 'enqueued') resolve(id);
else reject(new Error(e.detail.reason)); // invalid_arguments | enqueue_failed
}
window.addEventListener('appmint:work', onWork);
WebToApk.workEnqueueOnline(id, 'https://api.example.com/feedback', 'POST', '{}',
JSON.stringify({ rating: form.rating.value, text: form.text.value }));
});
}Guarda una nota como texto plano con PUT; fijar tu propio Content-Type evita que se añada el valor JSON por defecto:
function saveNote(noteId, text) {
if (!(window.WebToApk && window.WebToApk.workEnqueueOnline)) return;
WebToApk.workEnqueueOnline(
'note-' + noteId, // same note, same id: only the newest text is sent
'https://api.example.com/notes/' + encodeURIComponent(noteId),
'PUT',
JSON.stringify({ 'Content-Type': 'text/plain; charset=utf-8' }),
text
);
}
document.getElementById('note').addEventListener('change', function (e) {
saveNote(e.target.dataset.noteId, e.target.value);
});Notas: enqueued significa que Android aceptó el trabajo, no que el servidor lo recibió; no hay evento cuando se entrega. Reglas de entrega: una respuesta 2xx termina el trabajo; una respuesta 4xx lo detiene (sin reintento); un error de red o un 5xx reintenta con esperas crecientes, hasta 8 intentos. Content-Type: application/json se añade a menos que fijes uno. Un GET no envía cuerpo. El JavaScript de la página no se ejecuta en segundo plano: solo esta solicitud nativa.
workList puente#
window.WebToApk.workList(requestId: String)
No necesita permisos; los resultados se devuelven como `appmint:work` / window.onAppMintWork.
Se describe por su grupo, Trabajos en segundo plano (WorkManager): autoguardado duradero + sincronización nativa periódica, y no por separado.
Ejemplo
Lista los trabajos en segundo plano que esta app ha puesto en cola o programado.
Devuelve: nada directamente. El evento appmint:work (y window.onAppMintWork) responde { requestId, jobs: [{ id, periodic, url, createdAt, pendingDelivery }] }. pendingDelivery es true mientras un trabajo único aún no se ha entregado (y siempre para un trabajo periódico). Requiere: nada.
Un pequeño contenedor de Promise que empareja la respuesta por requestId:
function listJobs() {
return new Promise(function (resolve) {
if (!(window.WebToApk && window.WebToApk.workList)) { resolve([]); return; }
const requestId = 'jobs-' + Date.now();
function onWork(e) {
if (String(e.detail.requestId) !== requestId) return; // not our answer
window.removeEventListener('appmint:work', onWork);
resolve(e.detail.jobs || []);
}
window.addEventListener('appmint:work', onWork);
WebToApk.workList(requestId);
});
}
listJobs().then(function (jobs) {
const unsent = jobs.filter(function (j) { return !j.periodic && j.pendingDelivery; });
document.getElementById('unsent').textContent =
unsent.length ? unsent.length + ' change(s) waiting for network' : 'All changes saved';
});Muestra todos los trabajos en una pantalla de ajustes, con cuándo se pusieron en cola:
window.addEventListener('appmint:work', function (e) {
if (e.detail.requestId !== 'settings-jobs') return;
const list = document.getElementById('jobs');
list.innerHTML = '';
(e.detail.jobs || []).forEach(function (job) {
const li = document.createElement('li');
li.textContent = job.id + (job.periodic ? ' (repeats)' : job.pendingDelivery ? ' (waiting)' : '') +
' - since ' + new Date(job.createdAt).toLocaleString();
list.appendChild(li);
});
});
if (window.WebToApk && window.WebToApk.workList) WebToApk.workList('settings-jobs');Cuando la app se abre, fija un interruptor de "Sincronización en segundo plano" según si el trabajo repetitivo 'sync' sigue registrado:
window.addEventListener('appmint:work', function (e) {
if (e.detail.requestId !== 'startup') return;
const on = (e.detail.jobs || []).some(function (j) { return j.id === 'sync' && j.periodic; });
document.getElementById('sync-toggle').checked = on;
});
document.addEventListener('DOMContentLoaded', function () {
if (window.WebToApk && window.WebToApk.workList) WebToApk.workList('startup');
});Notas: un trabajo único sale de la lista una vez entregado, rechazado con un 4xx o abandonado tras 8 intentos. Un requestId numérico puede volver como número, así que compara con String(...).
workSchedulePeriodic puente#
window.WebToApk.workSchedulePeriodic(id: String, url: String, method: String, headersJson: String, payload: String, intervalMinutes: Int)
No necesita permisos; los resultados se devuelven como `appmint:work` / window.onAppMintWork.
Se describe por su grupo, Trabajos en segundo plano (WorkManager): autoguardado duradero + sincronización nativa periódica, y no por separado.
Ejemplo
Programa una solicitud HTTP repetitiva que Android envía en segundo plano (cuando hay conexión), incluso con la app cerrada. Útil para un disparador de sincronización o una señal de vida.
Devuelve: nada directamente. La respuesta llega como el evento appmint:work (y window.onAppMintWork(detail)): { id, status: 'scheduled', intervalMinutes }, o { id, status: 'failed', reason } con motivo invalid_arguments o schedule_failed. Requiere: nada.
function startBackgroundSync(userId) {
if (!(window.WebToApk && window.WebToApk.workSchedulePeriodic)) {
showMessage('Background sync works in the installed app only.');
return;
}
WebToApk.workSchedulePeriodic(
'sync', // job id (scheduling again updates it)
'https://api.example.com/sync',
'POST',
JSON.stringify({ 'X-User': userId }), // headers as a JSON string
JSON.stringify({ userId: userId }), // the same body is sent every time
30 // minutes; values under 15 become 15
);
}
// Optional page hook instead of addEventListener:
window.onAppMintWork = function (d) {
if (d.id === 'sync' && d.status === 'scheduled') {
console.log('sync every', d.intervalMinutes, 'minutes');
}
if (d.status === 'failed') console.warn('job', d.id, 'failed:', d.reason);
};Una señal de vida cada hora: un GET no envía cuerpo, así que pasa una cadena vacía, y '{}' cuando no necesites encabezados:
function startHeartbeat(deviceId) {
if (!(window.WebToApk && window.WebToApk.workSchedulePeriodic)) return;
WebToApk.workSchedulePeriodic(
'heartbeat',
'https://api.example.com/ping?device=' + encodeURIComponent(deviceId),
'GET',
'{}',
'',
60
);
}Deja que el usuario elija con qué frecuencia sincronizar y muestra el intervalo que Android realmente usa (cualquier valor menor de 15 vuelve como 15):
document.getElementById('sync-every').addEventListener('change', function (e) {
if (!(window.WebToApk && window.WebToApk.workSchedulePeriodic)) return;
const minutes = parseInt(e.target.value, 10); // e.g. 5, 30, 120
WebToApk.workSchedulePeriodic('sync', 'https://api.example.com/sync', 'POST', '{}',
JSON.stringify({ reason: 'periodic' }), minutes); // same id: updates the existing job
});
window.addEventListener('appmint:work', function (e) {
const d = e.detail;
if (d.id !== 'sync') return;
if (d.status === 'scheduled') {
document.getElementById('sync-note').textContent = 'Syncing about every ' + d.intervalMinutes + ' minutes';
} else if (d.status === 'failed') {
document.getElementById('sync-note').textContent = 'Could not schedule sync (' + d.reason + ')';
}
});Notas: Android decide el momento exacto; 15 minutos es el intervalo más corto y el ahorro de batería puede retrasar las ejecuciones. El cuerpo se fija al programar; para cambiarlo, programa de nuevo con el mismo id. Una respuesta 4xx no cancela un trabajo periódico; la siguiente ejecución lo intenta de nuevo. Deténlo con workCancel('sync').