apacuana_sdk_flutter 0.6.8
apacuana_sdk_flutter: ^0.6.8 copied to clipboard
Apacuana SDK Core proporciona una interfaz para interactuar con los servicios de Apacuana.
apacuana_sdk_flutter #
Apacuana SDK para Flutter es un wrapper de alto nivel construido sobre apacuana_sdk_core_dart. Proporciona un Provider y un Notifier (basado en ChangeNotifier) que simplifican la integración de los servicios de Apacuana en apps Flutter, manejando:
- Inicialización del SDK y estado de servicio.
- Generación de certificados digitales en el dispositivo.
- Firma electrónica de documentos.
- Consulta del estado de certificados y documentos.
- Gestión de revocaciones.
- Flujos de Face Liveness con un modal embebido.
Este paquete abstrae detalles de estado, modales y almacenamiento local cifrado, exponiendo una API declarativa y fácil de usar desde widgets.
Índice #
- Instalación
- Inicialización rápida
- Arquitectura: Provider + Notifier
- Configuración del SDK (config)
- API del Notifier (métodos y ejemplos)
- getConfig
- getCustomer
- generateCert
- isCertificateInDevice
- getCertStatus
- getCertTypes
- getRequerimentsByTypeUser
- addSigner
- getDocsByCustomer
- signDocument
- startLivenessCheck
- validateFaceLiveness
- requestRevocation
- getRevocationReasons
- createApacuanaUser
- uploadSignatureVariant
- getSignatureVariant
- deleteSignatureVariant
- Tipos útiles (ApiResult, CertData, DocsQuery)
- Permisos y configuración de plataforma
- Buenas prácticas y manejo de errores
- Ejemplo de app
- Licencia
Instalación #
En tu pubspec.yaml:
dependencies:
flutter:
sdk: flutter
provider: ^6.0.0
dio: ^5.4.0
apacuana_sdk_flutter: ^0.1.0 # Este paquete
Luego:
flutter pub get
Inicialización rápida #
Envuelve tu aplicación con el ApacuanaProvider. Este inicializa automáticamente el core cuando apiUrl y apiKey están presentes, y presenta los modales del SDK sobre tu UI.
Parámetros clave del config:
apiUrl: URL base de la API de Apacuana.secretKey: Llave secreta del cliente.apiKey: Clave pública de API (identifica tu integración).verificationId: Identificador de verificación inicial (usado en algunos flujos como onpremise).customerId(opcional): Usuario ya autenticado en tu sistema; si se provee el SDK intentará cargar su contexto.encryptionKey: Clave simétrica para cifrar datos sensibles localmente (elige una distinta por entorno, 16/24/32 bytes para AES).integrationType: Tipo de integración (ej.ONBOARDING, puede cambiar según el flujo que estés habilitando).
Ejemplo completo de inicialización (entorno dev):
import 'package:flutter/material.dart';
import 'package:apacuana_sdk_flutter/providers/apacuana_provider.dart';
import 'package:apacuana_sdk_flutter/providers/apacuana_notifier.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
// Puedes obtener el customerId de un login previo o almacenamiento seguro
final String? customerId = null; // Reemplaza con tu lógica
final config = {
'apiUrl': 'https://api.dev.apacuana.3dlinkweb.com',
'secretKey': 'fvTaYQzi1tubvKVZ2P9Ev3nw7v9WyEGh5k7PM9HDz8AB9J9uygJf76ihW9mTa8T08pv4Ww7XLY9r4B5KDc4BDikMJ6KGUJnX85CNh6QwDvBrS1apb5e3VVTWgHug7Y5f',
'apiKey': '07NTZW6cCgSCKjhzSe1UJ2uJ6fTXHaAFKcfDtJqV5Vnhg0h2BitNr6iYCeM7ZZ7CpcAVcQqk4EP0VmxvWPkTgBczpa8mGZjSU2V4zr56CpqLUM1AfjvXw79Gt4nppiTW',
'verificationId': '53d1daa5-cc7a-4a70-ae46-1a3e8a405f7a',
if (customerId != null && customerId.isNotEmpty) 'customerId': customerId,
'encryptionKey': 'estaesmiclavesecretaAES256123456',
'integrationType': 'ONBOARDING',
};
return MaterialApp(
title: 'Apacuana Demo',
builder: (context, child) {
return ApacuanaProvider(
config: config,
child: child ?? const SizedBox.shrink(),
);
},
home: const HomePage(),
);
}
}
// Cómo obtener el notifier:
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
final notifier = ApacuanaProvider.of(context, listen: true);
return Scaffold(
appBar: AppBar(title: const Text('Demo Apacuana')),
body: Center(child: Text('Estado SDK: ${notifier.status}')),
);
}
}
Arquitectura: Provider + Notifier #
El paquete expone:
ApacuanaProvider(Widget): inyecta y gestiona unApacuanaNotifieren el árbol de widgets, inicializa el core y renderiza un modal propio (ApacuanaModal) por encima de tu UI cuando el SDK lo requiere (por ejemplo, para el flujo de liveness).ApacuanaNotifier(ChangeNotifier): contiene el estado del SDK y los métodos de alto nivel para llamar a los servicios (certificados, firmas, liveness, revocaciones, etc.). Es el punto de entrada en tu UI para realizar operaciones.
Ventajas:
- Inicialización automática.
- Un solo punto de acceso al SDK desde la UI (
ApacuanaProvider.of(context)). - Modal gestionado por el SDK (aparecerá cuando el flujo lo requiera).
Configuración del SDK (config) #
El config que pasas al ApacuanaProvider es un Map<String, dynamic> que el core usa para inicializarse:
apiUrl(String, requerido): URL base del backend.apiKey(String, requerido): clave de API si tu backend la requiere.encryptionKey(String, recomendado): clave para cifrar datos sensibles (claves privadas, certificados) en el dispositivo.customerId(String, opcional): si ya tienes sesión de usuario, permite inicializar con token y userData.- Otros campos que tu core soporte (p. ej. entorno, timeouts, etc.).
El Provider hace:
- Si
apiUrlyapiKeyexisten y no están vacíos:notifier.initCore(config). - Si
customerIdestá presente, el core intentará recuperar token/userData y configurar autenticación.
API del Notifier (métodos y ejemplos) #
Para obtener el notifier:
final apn = ApacuanaProvider.of(context);
El estado del servicio:
apn.status:ApacuanaServiceStatus(idle | initializing | ready | error)apn.error: último error (si hubo) durante la init.
Importante: muchas operaciones requieren que el SDK esté ready.
getConfig #
- Firma:
Future<SdkConfig?> getConfig() - Requiere: SDK inicializado.
- Retorno: objeto de configuración actual (SdkConfig) o null si falla.
Ejemplo:
final cfg = await apn.getConfig();
debugPrint('customerId: ${cfg?.customerId}, apiUrl: ${cfg?.apiUrl}');
getCustomer #
- Firma:
Future<ApacuanaSuccess> getCustomer() - Requiere: customerId (sesión)
- Retorno:
ApacuanaSuccesscondatacontendo token/userData.
Ejemplo:
final res = await apn.getCustomer();
debugPrint('getCustomer success: ${res.success}, data: ${res.data}');
generateCert #
-
Firma:
Future<ApiResult> generateCert({ required String pin, bool enableSaveWithBiometrics = false }) -
Requiere: SDK en estado
readyy customerId. IMPORTANTE: el usuario debe estar previamente aprobado por tu backend (o por el proceso de verificación que uses) antes de intentar generar el certificado. -
Descripción: Genera un par de claves en el dispositivo, crea un CSR, lo envía al core/backend y almacena localmente la clave privada y el certificado cifrados.
-
Recomendación de flujo eficiente:
- Antes de llamar a
generateCert, consultagetCertStatus()y valida que el estado del certificado sea "Por generar". Si no está en ese estado, evita generar el certificado y muestra un mensaje al usuario o reintenta según tu lógica. - Si
getCertStatus()indica "Por generar", llama agenerateCert(pin: ...).
- Antes de llamar a
-
Retorno:
ApiResultcondata: CertData(cert y certifiedId) cuando aplica.
Ejemplo recomendado:
// 1) Comprobar estado
final statusRes = await apn.getCertStatus();
if (statusRes['success'] == true) {
final remoteState = (statusRes['data'] != null && statusRes['data']['state'] != null)
? statusRes['data']['state'].toString()
: (statusRes['data'] != null && statusRes['data']['status'] != null)
? statusRes['data']['status'].toString()
: '';
if (remoteState.toLowerCase().contains('por generar')) {
// 2) Generar certificado
final res = await apn.generateCert(pin: '1234', enableSaveWithBiometrics: false);
if (res.success) {
// éxito
} else {
// manejar error
}
} else {
// Usuario no está en estado "Por generar". Mostrar mensaje o actuar según tu flujo.
}
} else {
// No se pudo consultar el estado: manejar error/mostrar feedback
}
Ejemplo:
final res = await apn.generateCert(pin: '1234', enableSaveWithBiometrics: false);
if (res.success) {
final certData = res.data as CertData?;
debugPrint('Cert creado: id=${certData?.certifiedId}');
} else {
debugPrint('Error: ${res.message}');
}
isCertificateInDevice #
- Firma:
Future<bool> isCertificateInDevice() - Requiere: SDK en
ready. - Retorno: true si hay un certificado guardado cifrado en el dispositivo para ese customerId.
Ejemplo:
final hasCert = await apn.isCertificateInDevice();
getCertStatus #
- Firma:
Future<Map<String, dynamic>> getCertStatus() - Requiere: SDK
readyy customerId. - Descripción: Pregunta al core el estado del certificado (normaliza la respuesta a Map encodable).
- Retorno: Map con
success,data, etc. El campodatasuele contener información sobre el estado del proceso de certificación.
Estados posibles y su significado (valores mostrados como ejemplo — adapta a tu backend si usan otros textos):
Por verificar— El usuario ha iniciado el flujo pero aún no ha aportado la información/documentación necesaria para la verificación.En revisión— La documentación o resultados están siendo revisados por el sistema.Por generar— El usuario está aprobado y el certificado puede generarse: este es el estado en el que conviene invocargenerateCert().Vigente— El certificado fue emitido y está activo en el sistema.Por revocar— Se ha solicitado revocación pero aún no se ha procesado.Revocado— El certificado fue revocado y ya no es válido.Verificado— Estado que puede indicar que la identidad fue verificada correctamente (a veces equivalente aPor generar).
Manejo sugerido por estado (ejemplo corto):
final status = await apn.getCertStatus();
if (status['success'] == true) {
final data = status['data'] ?? {};
final state = (data['state'] ?? data['status'] ?? data['phase'] ?? '').toString().toLowerCase();
if (state.contains('por generar') || state.contains('ready') || state.contains('verified')) {
// Generar certificado (el usuario está aprobado o listo)
} else if (state.contains('por verificar') || state.contains('pending')) {
// Indicar al usuario que complete la verificación
} else if (state.contains('en revisión') || state.contains('review')) {
// Mostrar estado de revisión y evitar generar certificado
} else if (state.contains('vigente') || state.contains('issued')) {
// Mostrar que ya tiene certificado vigente
} else if (state.contains('por revocar') || state.contains('revoked') || state.contains('revocar')) {
// Tratar la revocación o informar al usuario
} else {
// Manejo por defecto
}
} else {
// Error consultando estado
}
Recomendación:
- Implementa comparaciones tolerantes (tolower/contains) y un mapeo local de valores del backend a los estados semánticos anteriores para evitar romper la UI si cambian las etiquetas exactas en el servidor.
- Evita generar certificados hasta que el estado indique explícitamente que está "Por generar" o su equivalente.
getCertTypes #
- Firma:
Future<dynamic> getCertTypes() - Requiere: público (no requiere customerId).
- Descripción: Devuelve la lista de tipos de certificado disponibles en el backend/core. Cada elemento suele contener un
idy metadatos sobre requisitos para ese tipo de certificado. - Importancia: Antes de crear un usuario con
createApacuanaUser, necesitas conocer qué tipo de certificado usará esa persona.getCertTypes()te permite obtener eliddel tipo que luego debes incluir en el payload decreateApacuanaUser(por ejemplo, comotypeuser).
Retorno: una lista/estructura con los tipos. La forma exacta depende del core, por ejemplo:
[
{ "id": "1", "name": "Certificado A", "requires": [ ... ] },
{ "id": "2", "name": "Certificado B", "requires": [ ... ] }
]
Ejemplo práctico (obtener tipos y usar el id al crear usuario):
// 1) Obtener tipos disponibles
final typesRes = await apn.getCertTypes();
// typesRes puede ser List<dynamic> o un wrapper; adapta según tu core
final List<dynamic> types = typesRes is List ? typesRes : (typesRes['data'] ?? []);
// 2) Buscar el tipo deseado (por nombre o criterio)
final desired = types.firstWhere(
(t) => (t['name'] ?? '').toString().toLowerCase().contains('certificado a'),
orElse: () => null,
);
if (desired == null) {
// manejar: tipo no disponible
}
final typeuser = desired['id'].toString();
// 3) Crear payload de usuario incluyendo el tipo de certificado
final payload = {
'kinddoc': 'V',
'doc': '12345678',
'email': 'usuario@correo.com',
'name': 'Nombre',
'lastname': 'Apellido',
'phone': 4141234567,
'typeuser': typeuser,
};
final res = await apn.createApacuanaUser(payload);
Notas:
- El nombre del campo (
typeuseren el ejemplo) y la estructura del resultado degetCertTypes()pueden variar. Revisa la respuesta real y adapta las claves. - Obtener y validar el tipo antes de crear el usuario evita errores y asegura que el usuario quede configurado con el certificado correcto desde el inicio.
- Tras crear el usuario, tu backend puede devolver el estado inicial del certificado (ver
getCertStatus) para continuar el flujo.
getRequerimentsByTypeUser #
- Firma:
Future<dynamic> getRequerimentsByTypeUser({ required int type }) - Requiere: público.
- Parámetros:
type(int): tipo de usuario.
- Descripción: Devuelve un listado de los requisitos (documentos, comprobantes, imágenes, etc.) que el backend necesita para crear un usuario de ese tipo. Cada requisito normalmente incluye un
id,namey metadatos sobre el tipo de archivo esperado. - Uso clave: al crear el usuario con
createApacuanaUser, debes adjuntar los archivos requeridos usando claves con el formatofile-{id}donde{id}es elidque devuelvegetRequerimentsByTypeUserpara cada documento. El valor debe ser unMultipartFile(por ejemplodio.MultipartFile).
Retorno: lista/estructura con los requisitos. Ejemplo aproximado:
[
{ "id": 1, "name": "Cédula de identidad" },
{ "id": 2, "name": "Rif" },
]
Ejemplo práctico completo (obtener requisitos y crear usuario adjuntando archivos):
import 'package:dio/dio.dart' show MultipartFile;
// 1) Obtener requisitos para el tipo de usuario
final reqsRes = await apn.getRequerimentsByTypeUser(type: 1);
final List<dynamic> reqs = reqsRes is List ? reqsRes : (reqsRes['data'] ?? []);
// 2) Preparar payload básico del usuario
final payload = {
'kinddoc': 'V',
'doc': '12345678',
'email': 'usuario@correo.com',
'name': 'Nombre',
'lastname': 'Apellido',
'phone': 4141234567,
// puedes incluir campos adicionales que tu backend requiera
};
// 3) Adjuntar archivos según requisitos: cada archivo debe usar la clave 'file-{id}'
final Map<String, dynamic> filesToUpload = {};
for (final r in reqs) {
final reqId = r['id']?.toString();
if (reqId == null) continue;
// En tu app obtén la ruta del archivo correspondiente al requisito (picker, cámara, etc.)
final localPath = await pickFilePathForRequirement(r);
if (localPath == null) continue; // usuario no proporcionó ese archivo
final multipart = await MultipartFile.fromFile(localPath, filename: localPath.split('/').last);
filesToUpload['file-$reqId'] = multipart;
}
// 4) Combinar payload y archivos en el body que envía createApacuanaUser
final combined = {...payload, ...filesToUpload};
// 5) Llamada para crear usuario
final res = await apn.createApacuanaUser(combined);
// Manejo del resultado
if (res != null && (res['success'] == true || (res is Map && res['success'] == true))) {
// Usuario creado
} else {
// Error
}
Notas y recomendaciones:
pickFilePathForRequirementes una función de ejemplo: implementa tu flujo para que el usuario seleccione/ capture cada documento requerido.- Asegúrate de validar los tipos esperados y el tamaño máximo antes de crear
MultipartFile. - Dependiendo
createApacuanaUserpuede esperar unFormData/multipart en lugar de un simple Map; el ejemplo usadio.MultipartFileque es compatible conFormData. - Verifica la estructura concreta que tu core espera (nombres de campos, nombre exacto
file-{id}, etc.) y adapta según sea necesario. - Este enfoque garantiza que cada archivo se asocie al requisito correcto mediante su
id, facilitando el proceso de validación en backend.
addSigner #
- Firma:
Future<dynamic> addSigner(Map<String, dynamic> signer) - Requiere: customerId.
- Descripción: Esta función permite agregar un firmante a un documento. Al ejecutarse correctamente, retorna un
id(osignatureId) que debe ser usado posteriormente en la funciónsignDocumentpara firmar el documento. - Parámetros (todos obligatorios):
doc(String): cédula del usuario (identificación). Ejemplo: '12345678'.name(String): nombre del usuario. Ejemplo: 'Juan'.typedoc(String): tipo de cédula, debe ser uno de los siguientes valores: 'V', 'E', 'P'.file(MultipartFile): documento a firmar (por ejemplo, PDF o imagen). Usadio.MultipartFile.signature(List<Map<String, dynamic>>): array de mapas, cada uno representa una posición de firma y debe contener:x(double): posición horizontal de la firma, valor porcentual entre 0 y 1 (0.0 = extremo izquierdo, 1.0 = extremo derecho). Ejemplo: 0.3 equivale al 30% desde la izquierda.y(double): posición vertical de la firma, valor porcentual entre 0 y 1 (0.0 = parte superior, 1.0 = parte inferior). Ejemplo: 0.4 equivale al 40% desde arriba.page(int): número de página donde se colocará la firma (comenzando en 1).
Nota: El punto (0,0) corresponde a la esquina superior izquierda de la hoja. Los valores de
xyyson proporcionales al ancho y alto de la página respectivamente.
Ejemplo de uso
import 'package:dio/dio.dart';
final file = await MultipartFile.fromFile('/ruta/al/documento.pdf', filename: 'documento.pdf');
final signerPayload = {
'doc': '12345678',
'name': 'Juan',
'typedoc': 'V',
'file': file,
'signature': [
{
'x': 0.3, // 30% desde la izquierda
'y': 0.4, // 40% desde arriba
'page': 1,
},
// Puedes agregar más posiciones si el documento requiere varias firmas
],
};
final addRes = await apn.addSigner(signerPayload);
// Extraer signatureId del resultado, cubriendo los posibles formatos de respuesta
final String signatureId = (() {
try {
final dyn = addRes as dynamic;
final d = dyn.data ?? dyn['data'];
if (d is Map) {
return (d['signatureId'] ?? d['id'] ?? d['signature']?['id'] ?? '').toString();
}
} catch (_) {}
if (addRes is Map<String, dynamic>) {
final d = addRes['data'];
if (d is Map) {
return (d['signatureId'] ?? d['id'] ?? d['signature']?['id'] ?? '').toString();
}
}
return '';
})();
if (signatureId.isNotEmpty) {
// Usa signatureId en signDocument
print('signatureId: ' + signatureId);
} else {
// Manejar error o respuesta inesperada
print('No se pudo obtener el signatureId');
}
getDocsByCustomer #
- Firma:
Future<dynamic> getDocsByCustomer({ required int page, required int size, int? status }) - Requiere: customerId.
- Parámetros:
page(int) > 0size(int) > 0status(int?) opcional
- Retorno: documentos paginados del usuario.
Ejemplo:
final res = await apn.getDocsByCustomer(page: 1, size: 20, status: null);
signDocument #
- Firma:
Future<ApiResult<dynamic>> signDocument({ required Map<String, dynamic> signature, required String pin, bool enableBiometric = false }) - Requiere: customerId, certificado y clave privada almacenados.
- Flujo recomendado: Antes de llamar a esta función, debes haber agregado el firmante usando
addSigner. Esta función retorna directamente un camposignature(mapa) que contiene tanto elidcomo las posiciones (positionsconx,y,page). Ese mismo mapsignaturedebe ser usado como parámetro ensignDocument. - Descripción: Esta función aplica la firma electrónica sobre el documento previamente registrado. El parámetro
signaturedebe ser el mismo map retornado poraddSigner. El parámetropines la contraseña que el usuario definió al generar su certificado electrónico. - Parámetros:
signature(Map):id(String): id de la firma/documento, retornado dentro del camposignaturedeaddSigner.positions(Listdocument(MultipartFile o String base64, opcional): documento a firmar (solo si el backend lo requiere en este paso).
pin(String): contraseña definida por el usuario al generar el certificado.enableBiometric(bool): si es true y el usuario tiene biometría habilitada, solicitará autenticación biométrica.
- Retorno:
ApiResultcon la respuesta del core.
Importante:
- Siempre debes llamar primero a
addSignerpara registrar el firmante. Esta función retorna un camposignatureque contiene elidy las posiciones.- Usa ese mismo map
signaturecomo parámetro ensignDocument.- El campo
documentsolo es necesario si el backend lo requiere en este paso (en la mayoría de los flujos, el documento ya fue subido enaddSigner).- El
pines el password que el usuario eligió al generar su certificado electrónico.
Ejemplo de flujo completo
import 'package:dio/dio.dart';
// 1. Agregar firmante y obtener el map signature
final file = await MultipartFile.fromFile('/ruta/al/documento.pdf', filename: 'documento.pdf');
final signerPayload = {
'doc': '12345678',
'name': 'Juan',
'typedoc': 'V',
'file': file,
'signature': [
{'x': 0.3, 'y': 0.4, 'page': 1},
],
};
final addRes = await apn.addSigner(signerPayload);
final Map<String, dynamic> signature = addRes['signature'];
// 2. Firmar el documento usando el map signature y el pin del usuario
final pin = '1234'; // PIN definido por el usuario al generar el certificado
final res = await apn.signDocument(signature: signature, pin: pin);
if (res.success) {
print('Documento firmado correctamente');
} else {
print('Error al firmar: \\${res.message}');
}
startLivenessCheck #
- Firma:
Future<Map<String, dynamic>> startLivenessCheck() - Requiere: customerId.
- Descripción: Crea una sesión de liveness (via core), abre un modal con un WebView interno (
WebViewLiveness), y al completar valida con el backend. Cierra el modal automáticamente. - Retorno: Map con resultado normalizado (incluye
success,data, etc.). Puede devolver error por timeout o cancelación.
Ejemplo:
final res = await apn.startLivenessCheck();
if (res['success'] == true) {
// éxito
}
validateFaceLiveness #
- Firma:
Future<dynamic> validateFaceLiveness({ required String sessionId }) - Requiere: customerId.
- Parámetros:
sessionId(String): id de sesión creada previamente.
- Retorno: respuesta del core.
Ejemplo:
final res = await apn.validateFaceLiveness(sessionId: 'SESSION_ID');
requestRevocation #
- Firma:
Future<dynamic> requestRevocation(int reasonCode) - Requiere: customerId.
- Parámetros:
reasonCode(int): motivo de revocación.
- Retorno: respuesta del core.
Ejemplo:
final res = await apn.requestRevocation(3);
getRevocationReasons #
- Firma:
Future<dynamic> getRevocationReasons() - Requiere: customerId.
- Retorno: lista/mapa de motivos de revocación.
Ejemplo:
final res = await apn.getRevocationReasons();
createApacuanaUser #
- Firma:
Future<dynamic> createApacuanaUser(Map<String, dynamic> userData) - Requiere: público.
- Descripción: Crea (o actualiza) un usuario en Apacuana. Al crear correctamente, la respuesta incluye un identificador del usuario (id). Ese
customerIdes el valor que podrás usar en la configuración del SDK (customerIdenconfig) para inicializar Apacuana asociado a ese usuario.
Campos obligatorios mínimos:
kinddoc(String)doc(String o numérico)email(String)
Comportamiento:
- Si el usuario no existía, la función crea el usuario y devuelve un id que es el
customerIdasociado. - Si el usuario ya existe y vuelves a llamar la función con el mismo identificador (por ejemplo
doc/kinddoc), la llamada debe comportarse como una actualización: se modifican los campos enviados en el payload sin crear un duplicado.
Subida de documentos:
- Para adjuntar documentos solicitados por
getRequerimentsByTypeUser, incluye en el payload campos con nombrefile-{id}donde{id}es el id del requisito devuelto porgetRequerimentsByTypeUser. El valor debe ser unMultipartFile(dio.MultipartFile) y el request debe enviarse como multipart/FormData.
Reglas específicas:
- Si el tipo de usuario corresponde a "Profesional titulado" (según el
typeuser), entonces el campooccupationes obligatorio y debe ser enviado.
Campos adicionales (lista de los campos aceptados por la función):
{
email,
typeuser,
name,
lastname,
kinddoc,
doc,
birthdate,
kindrif,
gender,
rif,
phone,
kindphone,
state,
municipality,
parish,
postalcode,
address,
fiscaladdress,
fiscalkindphone,
fiscalphone,
occupation, // obligatorio para Profesional titulado
degree,
university,
graduationyear,
collegiatenumber,
collegiateyear,
companyname,
companykindrif,
companyrif,
companystate,
companymunicipality,
companyparish,
companyaddress,
companykindphone,
companyphone,
companypostalcode,
companywebpage,
companycommercialregister,
companyregisterdate,
companyregisternumber,
companyconstitutiondate,
companypublishdate,
companyconstitutiondecree,
companynumberdecree,
positionprivate,
departmentprivate,
authorizedprivate,
functionsprivate,
publishprivate,
issuedateprivate,
kindphoneprivate,
phoneprivate,
positionpublic,
departmentpublic,
authorizedpublic,
functionspublic,
publishpublic,
issuedatepublic,
kindphonepublic,
phonepublic,
companyid
}
Ejemplo práctico completo (crear usuario con archivos):
import 'package:dio/dio.dart' show MultipartFile, FormData;
// 1) Obtener tipos y requisitos (opcional)
final types = await apn.getCertTypes();
final reqs = await apn.getRequerimentsByTypeUser(type: selectedType);
// 2) Preparar campos del usuario
final payload = {
'kinddoc': 'V',
'doc': '12345678',
'email': 'usuario@correo.com',
'name': 'Nombre',
'lastname': 'Apellido',
'typeuser': selectedType,
// si selectedType corresponde a Profesional titulado, asegúrate de incluir 'occupation'
};
// 3) Adjuntar archivos según `reqs` (cada archivo con clave 'file-{id}')
final Map<String, dynamic> formMap = {};
formMap.addAll(payload);
for (final r in reqs) {
final reqId = r['id']?.toString();
if (reqId == null) continue;
final localPath = await pickFilePathForRequirement(r); // implementa picker
if (localPath == null) continue;
final multipart = await MultipartFile.fromFile(localPath, filename: localPath.split('/').last);
formMap['file-$reqId'] = multipart;
}
// 4) Enviar como FormData si el core lo requiere
final formData = FormData.fromMap(formMap);
final res = await apn.createApacuanaUser(formData.fields.asMap()); // adapta según la implementación del wrapper/core
// 5) Manejo de la respuesta: guardar customerId si se crea
if (res != null) {
final createdId = res['data'] != null ? res['data']['id'] ?? res['data']['customerId'] : null;
if (createdId != null) {
// Guarda createdId como customerId en tu configuración para inicializar Apacuana
}
}
Notas finales:
- Revisa la especificación para confirmar el nombre exacto del campo donde devuelve el id (puede ser
id,customerId,userId, etc.). - Valida los requisitos y tipos de archivos antes de crear
MultipartFile. - Al volver a llamar la función con los mismos datos de identidad (
kinddoc+doc) se debe actualizar el usuario en lugar de crear uno nuevo.
uploadSignatureVariant #
- Firma:
Future<dynamic> uploadSignatureVariant({ required MultipartFile file }) - Requiere: customerId.
- Parámetros:
file(MultipartFile dedio): imagen de la firma.
- Retorno: respuesta del core.
Ejemplo:
import 'package:dio/dio.dart';
final file = await MultipartFile.fromFile('/path/firmita.png', filename: 'firmita.png');
final res = await apn.uploadSignatureVariant(file: file);
getSignatureVariant #
- Firma:
Future<dynamic> getSignatureVariant() - Requiere: customerId.
- Retorno: imagen/variant de firma del usuario (estructura según backend).
Ejemplo:
final res = await apn.getSignatureVariant();
deleteSignatureVariant #
- Firma:
Future<dynamic> deleteSignatureVariant() - Requiere: customerId.
- Retorno: resultado de la eliminación.
Ejemplo:
final res = await apn.deleteSignatureVariant();
Tipos útiles (ApiResult, CertData, DocsQuery) #
El Notifier expone y utiliza modelos simples para empaquetar resultados:
class ApiResult<T> {
final bool success;
final String message;
final T? data;
final String? code;
final dynamic error;
// ...
}
class CertData {
final String cert;
final String certifiedId;
// ...
}
class DocsQuery {
final int page;
final int size;
final int? status;
// ...
}
ApiResult<T>: respuesta segura para mostrar en UI (success,message,data,code,error).CertData: payload específico de certificados.DocsQuery: helper para consultas paginadas de documentos.
Permisos y configuración de plataforma #
Dependiendo de qué flujos uses (p. ej. Face Liveness que utiliza la cámara en WebView), debes añadir permisos en cada plataforma y, en Android, manejar permisos en tiempo de ejecución y/o la petición de permisos desde el WebView nativo.
A continuación se indican las acciones mínimas y recomendaciones por plataforma.
Android #
Archivos a editar:
android/app/src/main/AndroidManifest.xml
Permisos necesarios (añade dentro de la etiqueta <manifest>):
<!-- Permite acceso a internet (necesario para WebView) -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- Permiso de cámara para liveness -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- Indica que la app puede usar cámara, pero no la hace obligatoria -->
<uses-feature android:name="android.hardware.camera" android:required="false" />
Notas importantes para Android:
- Además del
AndroidManifest.xml, debes solicitar el permiso de cámara en tiempo de ejecución antes de abrir el WebView que usará getUserMedia. Recomendamos usar el paquetepermission_handler:
import 'package:permission_handler/permission_handler.dart';
final status = await Permission.camera.request();
if (!status.isGranted) {
// Maneja rechazo: muestra error o pide al usuario que otorgue permiso en Ajustes
}
- El contenido web que use getUserMedia (WebRTC) también solicita permisos dentro del WebView. Para garantizar que el WebView acepte esas peticiones debes usar una implementación que exponga
onPermissionRequest(por ejemploflutter_inappwebview) y conceder recursos solicitados:
InAppWebView(
androidOnPermissionRequest: (controller, origin, resources) async {
return PermissionRequestResponse(
resources: resources,
action: PermissionResponseAction.GRANT, // concede recursos (ver versión del plugin)
);
},
// ...
)
- Prueba en dispositivo físico. Emuladores pueden no exponer cámara o comportarse de forma distinta.
- Usa
adb logcatpara depurar errores del WebView y getUserMedia.
iOS #
Archivo a editar:
ios/Runner/Info.plist
Añade las descripciones de uso (claves y textos explicativos) para que iOS muestre la alerta al solicitar permisos:
<key>NSCameraUsageDescription</key>
<string>Usamos la cámara para verificar la vivacidad (liveness) en el flujo de verificación facial.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Se requiere acceso a la galería si se desea subir una imagen desde Fotos.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Se requiere acceso a la galería si se desea subir una imagen desde Fotos.</string>
Notas importantes para iOS:
- Con solo añadir estas claves, el sistema mostrará la alerta de permiso cuando el WebView solicite acceso a la cámara.
- Si usas
webview_flutteroflutter_inappwebview, getUserMedia generalmente funciona en iOS siempre que la página lo solicite y la claveNSCameraUsageDescriptionesté presente. - Prueba en dispositivo físico (simulador no siempre proporciona la cámara real).
Web #
- Si mantienes la opción Web que abre una pestaña (
WebViewLiveness), la cámara dependerá del navegador y sus permisos. Informa al usuario que debe permitir el acceso a la cámara en el prompt del navegador. - Asegúrate de servir la página sobre HTTPS (requisito para getUserMedia en la mayoría de navegadores).
Resumen de pasos recomendados (rápido) #
- Añadir permisos en
AndroidManifest.xmlyInfo.plistsegún corresponda. - Antes de abrir el WebView en Android solicita
Permission.cameraen tiempo de ejecución. - Usar
flutter_inappwebview(o la API dewebview_flutterque permita manejar requests de permisos) y concederandroidOnPermissionRequestpara recursos comoVIDEO_CAPTURE. - Probar en dispositivos reales y revisar
adb logcat/Xcode logs si algo falla.
Buenas prácticas y manejo de errores #
- Inicializa el SDK desde el arranque de la app, antes de llamar métodos (revisa
apn.status). - Usa try/catch. El core puede lanzar errores de tipo
ApacuanaAPIError; el Notifier suele envolver resultados enApiResulto normalizar aMap. - Protege datos sensibles:
- Usa
encryptionKeydistinta en producción. - No loguees claves privadas ni certificados completos.
- Usa
- Biometría:
- Ofrece fallback al PIN si el dispositivo no soporta biometría.
- Modales del SDK:
ApacuanaProviderrenderizaApacuanaModalautomáticamente cuando el Notifier lo requiere (p. ej., liveness). No necesitas gestionarlo manualmente.
- Estados comunes:
NOT_INITIALIZED_ERROR: espera a queapn.status == ApacuanaServiceStatus.readyo corrige tuconfig.- Errores de red: revisa conectividad y
apiUrl.
Ejemplo de app #
Revisa la carpeta example/ del repositorio (si está disponible) para una app que:
- Inicializa el SDK con
ApacuanaProvider. - Presenta botones para invocar cada método y muestra resultados en JSON.
Ejemplo mínimo de botón para generar certificado:
ElevatedButton(
onPressed: () async {
final res = await ApacuanaProvider.of(context).generateCert(pin: '1234');
if (res.success) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Cert OK')),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: ${res.message}')),
);
}
},
child: const Text('Generar certificado'),
)
Licencia #
Este proyecto se distribuye bajo la licencia MIT (ver archivo LICENSE en la raíz del paquete).