Plugins Cordova on-device para construir apps mobile con Forge. Todos comparten un mismo SDK slm.* expuesto desde el WebApiFramework.
Plugins siempre incluidos por default. Forman la base de cualquier app Forge.
Es la base sobre la que viven los demás plugins. Configurable via config.xml con la URL inicial del proyecto.
slm.showSplash();
slm.hideSplash();
slm.setStatusBarColor('#1B2A4A', { style: 'lightContent' });
slm.setStatusBarTransparent(true);
slm.hideStatusBar();
slm.showStatusBar();
slm.getNetworkInfo().then(r => {
// r = { type: 'wifi'|'cellular'|'none', online: bool }
});
slm.onNetworkChange(state => { /* ... */ });
Verificación de identidad on-device: documento + facial + biometría.
slm.startDocumentCamera({
x, y, width, height,
camera: 'back',
borderRadius: 16,
overlayHtml: '<html>...</html>',
overlayInteractive: false
});
slm.onDocumentDetected(e => { /* { detected, type } */ });
const r = await slm.captureDocument({
template: 'credential-front',
ocr: true,
quality: 0.9
});
// r.image, r.data { nombre, curp, ... }, r.text, r.blocks
slm.switchDocumentCamera();
slm.focusDocumentCamera();
slm.stopDocumentCamera();
Breaking 1.3.0: compareFaces se movió a face-verify. El SDK lo redirige automáticamente.
slm.startFaceVerify({
x, y, width, height,
borderRadius: 24,
overlayHtml: '<html>...</html>',
mirror: true
});
const selfie = await slm.captureSelfieForVerify();
// selfie.image (foto), selfie.aligned (crop 112x112)
const r = await slm.compareFaces({
image1: ineBase64,
image2: selfieBase64,
threshold: 60
});
// r.match, r.score (0-100), r.cosine, r.model
slm.closeFaceVerify();
Misma persona típica: cosine 0.6 → score 92. Distintas: cosine 0.3 → score 23. Threshold default 60.
No tiene peso legal. Para KYC formal complementar con AWS Rekognition / Azure Face API server-side.
slm.startLivenessCamera({
x, y, width, height,
smileThreshold: 0.3,
turnThreshold: 25, // grados
overlayHtml: '<html>...</html>'
});
slm.onLivenessEvent(e => { /* yaw, roll, smile, eyeL, eyeR */ });
slm.runLivenessChallenge({ challenge: 'blink' });
slm.captureLivenessPhoto();
slm.stopLivenessCamera();
Liveness verifica que es persona real (no foto/video). Face Verify verifica que es la misma persona del INE. Suelen complementarse.
const r = await slm.captureSignature({
title: 'Firma aquí',
subtitle: 'Contrato de apertura',
penColor: '#000',
penWidth: 3,
backgroundColor: '#fff',
showDate: true,
showLine: true
});
// r.image (PNG base64), r.width, r.height
await slm.startSignaturePad({
x: 20, y: 240, width: 320, height: 180,
borderRadius: 16,
penColor: '#1a1a1a', penWidth: 3,
backgroundColor: 'transparent', // deja ver el HTML de atrás
backgroundHtml: '<div style="...">Firme aquí</div>',
overlayHtml: '<div style="...">FIRMA DIGITAL</div>',
overlayInteractive: false, // taps pasan al pad
freezeHtml: '<div>Confirmando firma...</div>'
});
const sig = await slm.getSignature({
crop: true, // auto-crop al bounding box
background: 'white' // 'transparent' | hex (override del export)
});
// sig.image (PNG base64), sig.width, sig.height, sig.hasInk
await slm.clearSignature(); // limpia trazos
await slm.freezeSignaturePad(); // bloquea pad + muestra freezeHtml
await slm.unfreezeSignaturePad(); // re-habilita
await slm.stopSignaturePad(); // cierra y libera
De abajo hacia arriba: backgroundHtml (marco decorativo, gradiente, "X" de firmante) → canvas nativo (transparente cuando backgroundColor: 'transparent') → overlayHtml (botones flotantes, branding, hints). Con overlayInteractive: false los taps pasan al pad sin que el overlay los consuma. freezeHtml se pre-carga oculto y se muestra con freezeSignaturePad() — útil para "preview de firma" antes de confirmar.
const info = await slm.checkBiometric();
// { available, type: 'face'|'touch'|'fingerprint', enrolled }
const r = await slm.authenticateBiometric({
reason: 'Confirmar transferencia',
fallbackTitle: 'Usar PIN'
});
// r.success, r.error
Cámara, fotos, video, escaneo de códigos.
const r = await slm.takePicture({
quality: 0.85,
camera: 'back',
format: 'base64'
});
slm.captureImage(opts);
slm.captureVideo({ duration: 30 });
iOS 14+ usa PHPickerViewController. Android 13+ usa ACTION_PICK_IMAGES. Sin necesidad de permiso de galería completa.
const r = await slm.pickMedia({
type: 'image', // 'image' | 'video' | 'any'
multiple: true,
limit: 5
});
slm.scanQR() abre el lector a pantalla completa.slm.openQRPreview({x, y, width, height, overlayHtml}) coloca la cámara en un rect con overlay nativo.const r = await slm.scanQR();
// r.value, r.format ('qr', 'ean13', 'code128', ...)
slm.openQRPreview({
x, y, width, height,
overlayHtml: '<html>...marco verde + scan-line...</html>'
});
slm.onQRDetected(value => { /* ... */ });
slm.closeQRPreview();
const info = await slm.getDeviceInfo();
// { model, modelName, manufacturer, os, osVersion, uuid, locale }
Manejo de documentos: selección, visualización, descarga.
const r = await slm.pickFile({
types: ['pdf', 'image/*'],
multiple: false
});
// r.name, r.uri, r.size, r.mime, r.base64
slm.openPDF({
url: 'https://example.com/contrato.pdf',
// o:
base64: 'JVBERi0xLjQKJ...',
title: 'Contrato',
allowShare: true
});
slm.closePDF();
slm.downloadFile({
url: 'https://api/file.zip',
headers: { Authorization: 'Bearer ...' },
filename: 'state.zip',
openOnFinish: true
}, progress => console.log(progress.percent));
Ubicación, push, share, wallets.
const pos = await slm.getGPS({ accuracy: 'high' });
// pos.lat, pos.lng, pos.accuracy
const id = slm.watchGPS(pos => { /* updates */ });
slm.clearWatch(id);
slm.openLocationSettings();
const token = await slm.registerPush();
slm.onNotification(payload => { /* tapped o foreground */ });
// Cold-start: app abierta desde notif con killed state
const initial = await slm.getInitialNotification();
if (initial) { /* enrutar */ }
slm.registerPushWithBackend({
url: 'https://api/push/register',
headers: { Authorization: '...' },
extra: { userId: 'abc' }
});
slm.share({
title: 'Compartir',
text: 'Mira mi cuenta',
url: 'https://...',
image: 'iVBORw0KGgo...' // base64 (sin data: o con data URL)
});
slm.share({
pdf: 'JVBERi0xLjQK...', // base64 del PDF
fileName: 'factura.pdf', // opcional, default "documento.pdf"
text: 'Adjunto factura' // opcional, va como texto adicional
});
slm.share({
file: 'UEsDBAo...', // base64
fileName: 'reporte.xlsx',
mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
Acepta tanto base64 puro como data URL (data:application/pdf;base64,...). Pasa solo uno de image/pdf/file por llamada. Los archivos se guardan en cache temporal y se limpian a la hora.
const r = await slm.provisionCard({
issuerToken: '...', // del backend del banco
cardholder: 'Sergio Guerrero',
last4: '1234'
});
Integridad, attestation, deep links, permisos.
const info = await slm.checkIntegrity();
// { jailbroken, debugger, emulator, screenRecording, suspicious }
slm.preventScreenshots(true);
slm.onScreenRecord(active => { /* ... */ });
const token = await slm.attestApp({
nonce: 'server-generated-nonce'
});
// Mandar token al backend para validación con Apple/Google
slm.onDeepLink(url => { /* ej: https://app.com/ref/ABC */ });
const initial = await slm.getInitialDeepLink();
// URL que abrió la app desde killed state, o null
const s = await slm.checkPermission('camera');
// { granted, denied, restricted, notDetermined }
const r = await slm.requestPermission('location');
slm.openAppSettings(); // si fue denegado
await slm.getDeviceInfo(); // { model, modelName, os, osVersion, uuid }
Dictado por voz y control fino del teclado virtual.
slm.startDictation({ locale: 'es-MX' }, partial => {
// partial.text actualiza en tiempo real
});
const r = await slm.stopDictation();
// r.text final
slm.onKeyboardChange(state => {
// { visible, height, frame: { x, y, w, h } }
});
slm.showKeyboard(); // requiere input enfocado
slm.dismissKeyboard(); // cierre programático
Los plugins de cámara embebida montan un UIWindow (iOS) / FrameLayout (Android) encima del WebView principal. Los <div> que la webapp pinte quedan debajo de la cámara dentro del rect.
La solución: pasar overlayHtml u overlayUrl al plugin, que monta un WebView nativo transparente dentro del UIWindow del plugin, encima del preview. Ese overlay sí queda visible.
slm.startDocumentCamera({
x, y, width, height,
borderRadius: 16, // esquinas redondeadas reales (CALayer / Outline)
overlayHtml: '<html>...</html>', // HTML inline encima del preview
overlayUrl: 'https://...', // alternativa: URL externa
overlayInteractive: false // false (default): toques pasan al WebView principal
// true: el overlay captura toques (botones funcionan)
});
En lugar de calcular {x, y, width, height} a mano, usa un <div> placeholder con tu layout flex y mídelo con getBoundingClientRect():
// HTML: <div id="camera-zone" style="flex:1;margin:8px 16px;"></div>
function getCameraRect() {
const r = document.getElementById('camera-zone').getBoundingClientRect();
return {
x: Math.round(r.left),
y: Math.round(r.top),
width: Math.round(r.width),
height: Math.round(r.height)
};
}
// Doble requestAnimationFrame para asegurar layout listo
requestAnimationFrame(() => requestAnimationFrame(() => {
const rect = getCameraRect();
slm.startDocumentCamera({ ...rect, ...otherOpts });
}));
Toda página dentro del IAB debe esperar al SDK antes de llamar plugins. El bridge se establece via webkit.messageHandlers.cordova_iab (iOS) / equivalente Android.
const slm = new _SLMWAFK();
await slm.start(); // resuelve cuando el bridge nativo está listo
// ahora puedes llamar slm.* sin riesgo
El panel admin (/projects/<id>) lista todos los plugins con un toggle. Los desactivados se quitan del package.json antes del cordova prepare, así no engordan el APK/IPA.
Por ejemplo, apps fintech sin KYC visual desactivan cordova-plugin-slm-face-verify y se ahorran ~10 MB del modelo MobileFaceNet.