import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from 'react'; import type { ApacuanaSDKProviderProps, ApacuanaSDKContextValue, ApacuanaSDKState, CertKeyInfo, UploadSignatureVariantData, SignDocumentParams, } from '../types'; // Dependencias de terceros y utilidades import apacuanaCore from 'apacuana-sdk-core'; import * as forge from 'node-forge'; import { generateCertificateMobile, generateKeyPairMobile, } from '../utils/certificates'; import { encryptAndStoreValue, encryptData, getEncryptStoreValue, isPasswordCorrect, savePassword, } from '../utils/crypto'; import { signDigest } from '../utils/signatures'; import type { GetDocsParams } from 'apacuana-sdk-core/dist/api/signatures'; import { addUserWithBiometrics, handleBiometricAuth, isBiometricEnabledForUser, } from '../utils/biometric'; import { ApacuanaModal } from '../components/ApacuanaModal'; // import type { CertificateRequestParams } from 'apacuana-sdk-core/dist/types/certs'; import type { SignerData } from 'apacuana-sdk-core/dist/types/signatures'; import WebViewLiveness from '../components/WebViewLiveness'; import type { UserData } from 'apacuana-sdk-core/dist/types/users'; /** * Clave por defecto utilizada para la encriptación del CSR (Certificate Signing Request) * antes de ser enviado a la API. * @internal */ const keyDefault = 'dRgUkXp2s5v8y/B?'; /** * Contexto de React para el SDK de Apacuana. * Proporciona acceso al estado del SDK y a las funciones de interacción con la API. * Su valor es `undefined` por defecto y solo se define dentro de `ApacuanaProvider`. * @internal */ const ApacuanaContext = createContext( undefined ); /** * Componente Proveedor que gestiona el ciclo de vida y la configuración del SDK de Apacuana. * Debe envolver cualquier parte de la aplicación que necesite acceder a las funcionalidades del SDK. * @param {ApacuanaSDKProviderProps} props - Propiedades para configurar el proveedor, * incluyendo las credenciales del SDK y los componentes hijos. */ export const ApacuanaProvider = (props: ApacuanaSDKProviderProps) => { const { children, ...config } = props; /** * Estado interno del SDK que refleja su ciclo de vida. * - `idle`: Estado inicial. * - `initializing`: El SDK está en proceso de configuración. * - `ready`: El SDK está inicializado y listo para usarse. * - `error`: Ocurrió un error durante la inicialización. */ const [state, setState] = useState({ status: 'idle', error: null, }); const [isModalVisible, setIsModalVisible] = useState(false); const [modalContent, setModalContent] = useState(null); const openModal = useCallback((content: React.ReactNode) => { setModalContent(content); setIsModalVisible(true); }, []); const closeModal = useCallback(() => { setIsModalVisible(false); // Buena práctica: limpiar el contenido al cerrar para liberar memoria setModalContent(null); }, []); /** * Clave de encriptación utilizada para proteger los datos sensibles * (clave privada, certificados) en el almacenamiento seguro del dispositivo. */ const encryptKey = config?.encryptionKey || ''; /** * Devuelve la configuración actual con la que se inicializó el SDK Core. * @returns {object} El objeto de configuración del SDK. * @throws {Error} Si el SDK no está en estado 'ready'. */ const getConfig = useCallback(() => { if (state.status !== 'ready') { throw new Error('El SDK no está listo. No se puede llamar a getConfig.'); } return apacuanaCore.getConfig(); }, [state.status]); /** * Obtiene la información completa del cliente. * @returns {Promise} Una promesa que resuelve con los datos del cliente. * @throws {Error} Si el SDK no está en estado 'ready'. */ const getCustomer = useCallback(async () => { if (state.status !== 'ready') { throw new Error( 'El SDK no está listo. No se puede llamar a getCustomer.' ); } return apacuanaCore.getCustomer(); }, [state.status]); /** * Genera un certificado digital en el dispositivo, crea el CSR, lo envía a la API y almacena las credenciales. * @param {string} pin - PIN para proteger el certificado. * @param {boolean} enableSaveWithBiometrics - Si se debe habilitar biometría para guardar el usuario. * @returns {Promise} Promesa con la respuesta de la API tras la emisión del certificado. * @throws {Error} Si el SDK no está listo o ocurre un error durante el proceso. */ const generateCert = useCallback( async (pin: string, enableSaveWithBiometrics: boolean = false) => { if (state.status !== 'ready') { throw new Error( 'El SDK no está listo. No se puede llamar a generateCertificate.' ); } try { const sdkConfig = apacuanaCore.getConfig(); const customerId = sdkConfig?.customerId; if (!pin) { return { success: false, message: 'Debe proporcionar un PIN para proteger el certificado.', code: 'ApacuanaSDKError', }; } if (enableSaveWithBiometrics) { const isAuthenticated = await handleBiometricAuth(); if (!isAuthenticated?.success) { return isAuthenticated; } await addUserWithBiometrics(customerId); } // 1. Generar par de claves (pública y privada) en el dispositivo. const keyPair = await generateKeyPairMobile(); // 2. Generar una Solicitud de Firma de Certificado (CSR) usando el par de claves. const { csr } = (await generateCertificateMobile(keyPair)) as { csr: string; }; // 3. Enviar el CSR encriptado a la API para su firma y emisión. const response: any = await apacuanaCore.generateCert({ csr: encryptData(csr, keyDefault), } as any); if (!response?.success) { return response; } // 4. Almacenar de forma segura la clave privada y el certificado emitido. await encryptAndStoreValue( `apacuana-pk-${customerId}`, forge.util.encode64(keyPair.privateKey), // Se guarda en Base64 para consistencia. encryptKey ); const keyCert = `apacuana-cert-${customerId}`; await encryptAndStoreValue(keyCert, response?.data?.cert, encryptKey); await encryptAndStoreValue( `apacuana-certId-${customerId}`, response?.data?.certifiedid, encryptKey ); await savePassword(`apacuana-pin-${customerId}`, pin); // 5. Actualizar la lista local de certificados del usuario para referencia futura. const certKeysRaw = await getEncryptStoreValue( 'apacuana-cert-keys', encryptKey ); const updatedKeys: CertKeyInfo[] = certKeysRaw ? JSON.parse(certKeysRaw) : []; if (!updatedKeys.some((item) => item.keyCert === keyCert)) { const userData: any = sdkConfig?.userData; const newCertInfo: CertKeyInfo = { keyCert, name: `${userData?.name?.value} ${userData?.lastname?.value}`, email: userData?.usr, rifPrefix: userData?.rif?.kindrif?.toLowerCase() || userData?.doc?.kinddoc?.toLowerCase() || '', rif: userData?.rif, docPrefix: userData?.doc?.kinddoc?.toLowerCase() || '', doc: userData?.doc?.doc || '', idUser: userData?.id, typeCert: 'cert', certificationId: userData?.certificationId ?? '', }; updatedKeys.push(newCertInfo); await encryptAndStoreValue( 'apacuana-cert-keys', JSON.stringify(updatedKeys), encryptKey ); } return response; } catch (error) { console.error( '[ApacuanaSDKError]: Error en generateCertificate:', error ); return { success: false, message: (error as Error)?.message || 'Error desconocido durante la generación del certificado.', code: 'ApacuanaSDKError', error, }; } }, [encryptKey, state.status] ); /** * Realiza una comprobación rápida si el certificado ya está almacenado en el dispositivo. * @returns {boolean} Promesa con la respuesta de la API tras la emisión del certificado. * @throws {Error} Si el SDK no está listo o ocurre un error durante el proceso. */ const isCertificateInDevice = useCallback(async () => { if (state.status !== 'ready') { throw new Error( 'El SDK no está listo. No se puede llamar a isCertificateInDevice.' ); } const sdkConfig = apacuanaCore.getConfig(); const customerId = sdkConfig?.customerId; const cert = await getEncryptStoreValue( `apacuana-cert-${customerId}`, encryptKey ); return !!cert; }, [state.status, encryptKey]); /** * Consulta el estado actual del certificado del cliente. * @returns {Promise} Promesa con el estado del certificado. * @throws {Error} Si el SDK no está listo. */ const getCertStatus = useCallback(async () => { if (state.status !== 'ready') { throw new Error( 'El SDK no está listo. No se puede llamar a getCertStatus.' ); } const sdkConfig = apacuanaCore.getConfig(); const customerId = sdkConfig?.customerId; const cert = await getEncryptStoreValue( `apacuana-cert-${customerId}`, encryptKey ); const isCertificateInDevice = !!cert; console.log('isCertificateInDevice:', isCertificateInDevice); return apacuanaCore.getCertStatus(isCertificateInDevice); }, [state.status, encryptKey]); /** * Obtiene los tipos de certificados disponibles para el cliente. * @returns {Promise} Promesa con los tipos de certificados. * @throws {Error} Si el SDK no está listo. */ const getCertTypes = useCallback(() => { if (state.status !== 'ready') { throw new Error( 'El SDK no está listo. No se puede llamar a requestRevocation.' ); } return apacuanaCore.getCertTypes(); }, [state.status]); /** * Obtiene los requisitos para un tipo de usuario específico. * @param {Object} data - Objeto con el tipo de usuario. * @returns {Promise} Promesa con los requisitos. * @throws {Error} Si el SDK no está listo. */ const getRequerimentsByTypeUser = useCallback( (data: { type: number }) => { if (state.status !== 'ready') { throw new Error( 'El SDK no está listo. No se puede llamar a requestRevocation.' ); } return apacuanaCore.getRequerimentsByTypeUser(data); }, [state.status] ); /** * Agrega un firmante a un documento. * @param {SignerData} signerData - Datos del firmante a agregar. * @returns {Promise} Promesa con la respuesta de la API. * @throws {Error} Si el SDK no está listo. */ const addSigner = useCallback( async (signerData: SignerData) => { if (state.status !== 'ready') { throw new Error( 'El SDK no está listo. No se puede llamar a addSigner.' ); } try { const responseAddSigner = (await apacuanaCore.addSigner( signerData )) as any; if (!responseAddSigner?.success) { return responseAddSigner; } const docId = responseAddSigner?.data?.docId; const docs = await apacuanaCore.getDocs({ page: 0, size: 10 }); const doc = docs?.data?.records[0] as any; return { success: true, data: { docId, doc, signature: doc?.signaturedata ?? {}, }, name: 'ApacuanaSuccess', statusCode: 200, }; } catch (error) { return { success: false, message: (error as Error)?.message || 'Error desconocido al agregar firmante.', code: 'ApacuanaSDKError', error, }; } }, [state.status] ); /** * Obtiene una lista paginada de documentos del cliente. * @param {GetDocsParams} data - Parámetros de paginación y filtrado. * @returns {Promise} Promesa con la lista de documentos. * @throws {Error} Si el SDK no está listo. */ const getDocsByCustomer = useCallback( (data: GetDocsParams) => { if (state.status !== 'ready') { throw new Error('El SDK no está listo. No se puede llamar a getDocs.'); } return apacuanaCore.getDocs(data); }, [state.status] ); /** * Orquesta el proceso de firma de un documento. La clave privada nunca abandona el dispositivo. * @param {SignDocumentParams} params - Datos de la firma, pin y biometría. * @returns {Promise} Promesa con la respuesta de la API tras la firma. * @throws {Error} Si no se provee una firma, no se encuentran credenciales o falla la comunicación. */ const signDocument = useCallback( async ({ signature, pin, enableBiometric }: SignDocumentParams) => { if (!signature) { // throw new Error('[ApacuanaSDKError]: Debe incluir una firma válida.'); return { success: false, message: 'Debe incluir una firma válida.', code: 'ApacuanaSDKError', }; } try { const sdkConfig = apacuanaCore.getConfig(); const customerId = sdkConfig?.customerId; const isBiometricEnable = await isBiometricEnabledForUser(customerId); const isPasswordSuccess = await isPasswordCorrect( `apacuana-pin-${customerId}`, pin ); if (isBiometricEnable?.success && enableBiometric) { const isAuthenticated = await handleBiometricAuth(); if (!isAuthenticated?.success) { return isAuthenticated; } } else if (!isPasswordSuccess) { return { success: false, message: 'PIN incorrecto. No se puede proceder con la firma.', code: 'ApacuanaSDKError', }; } // 1. Recuperar credenciales (certificado y clave privada) del almacenamiento seguro. const cert = await getEncryptStoreValue( `apacuana-cert-${customerId}`, encryptKey ); if (!cert) { return { success: false, message: 'No se encontró un certificado válido para el usuario. La firma no puede continuar.', code: 'ApacuanaSDKError', }; } const privateKey = await getEncryptStoreValue( `apacuana-pk-${customerId}`, encryptKey ); if (!privateKey) { return { success: false, message: 'No se encontró una clave privada válida para el usuario. La firma no puede continuar.', code: 'ApacuanaSDKError', }; } // 2. Obtener el 'digest' (hash) del documento a firmar desde la API. const digestBody = signature?.document ? { cert, signatureId: signature?.id, document: signature?.document, } : { cert, signatureId: signature?.id, }; const digestData = await apacuanaCore.getDigest(digestBody); console.log('[digestData]:', digestData); if (!digestData?.data?.digest) { return digestData; } // 3. Firmar el digest localmente usando la clave privada del dispositivo. const signedDigest = await signDigest( digestData?.data?.digest, privateKey ); const signBody = { signature: { id: signature?.id, positions: signature.positions, }, cert, signedDigest, document: signature?.document ? signature?.document : undefined, }; const response = await apacuanaCore.signDocument(signBody); return response; } catch (error) { return { success: false, message: 'Error al firmar el documento', code: 'ApacuanaSDKError', error: error, }; } }, [encryptKey] ); /** * Sube una variante de firma (imagen) para un firmante específico. * Esta imagen se utilizará como la representación gráfica de la firma en los documentos. * @param {UploadSignatureVariantData} data - Datos de la imagen y firmante. * @returns {Promise} Promesa con la respuesta de la API. * @throws {Error} Si el SDK no está listo. */ const uploadSignatureVariant = useCallback( (data: UploadSignatureVariantData) => { if (state.status !== 'ready') { throw new Error( 'El SDK no está listo. No se puede llamar a requestRevocation.' ); } return apacuanaCore.uploadSignatureVariant(data); }, [state.status] ); /** * Obtiene la variante de firma (imagen) de un firmante. * @returns {Promise} Promesa con la imagen de la firma. * @throws {Error} Si el SDK no está listo. */ const getSignatureVariant = useCallback(() => { if (state.status !== 'ready') { throw new Error( 'El SDK no está listo. No se puede llamar a requestRevocation.' ); } return apacuanaCore.getSignatureVariant(); }, [state.status]); /** * Elimina la variante de firma (imagen) de un firmante. * @returns {Promise} Promesa con la respuesta de la API. * @throws {Error} Si el SDK no está listo. */ const deleteSignatureVariant = useCallback(() => { if (state.status !== 'ready') { throw new Error( 'El SDK no está listo. No se puede llamar a requestRevocation.' ); } return apacuanaCore.deleteSignatureVariant(); }, [state.status]); /** * Inicia el flujo de detección de fe de vida (liveness) facial y abre el modal correspondiente. * Genera un sessionId único y muestra el modal con la UI de liveness. * Retorna una Promesa que se resuelve cuando se dispara onComplete/onError/onCancel. */ const startLivenessCheck = useCallback(() => { return new Promise(async (resolve) => { try { const response = await apacuanaCore.createFaceLivenessSession(); const conf = apacuanaCore.getConfig(); console.log('faceLiveness sessionId:', response?.data?.sessionId); if (!response?.success) { resolve(response); return; } let settled = false; const settle = (payload: any) => { if (settled) return; settled = true; // closeModal(); resolve(payload); setTimeout(() => { closeModal(); }, 1000); }; openModal( { console.log('FaceLivenessSuccess', payload); settle( payload ?? { success: true, message: 'Fe de vida completado con éxito.', } ); }} onError={(e: any) => { console.log('FaceLivenessError', e); settle( e ?? { success: false, message: 'Error durante la verificación de liveness.', code: 'LIVENESS_ERROR', } ); }} onCancel={() => { console.log('FaceLivenessCanceled'); settle({ success: false, message: 'El usuario canceló la verificación de liveness.', code: 'LIVENESS_CANCELED', }); }} /> ); } catch (error) { console.error('Error al iniciar la sesión de liveness:', error); resolve({ success: false, message: 'Error al iniciar la sesión de liveness.', code: 'ApacuanaSDKError', error, }); } }); }, [openModal, closeModal]); /** * Valida el resultado de una sesión de prueba de vida. * @param {Object} data - Objeto con el sessionId de la sesión de liveness. * @returns {Promise} Promesa con el resultado de la validación. * @throws {Error} Si el SDK no está listo. */ const validateFaceLiveness = useCallback( (data: { sessionId: string }) => { if (state.status !== 'ready') { throw new Error( 'El SDK no está listo. No se puede llamar a requestRevocation.' ); } return apacuanaCore.validateFaceLiveness(data); }, [state.status] ); /** * Inicia una solicitud para revocar el certificado del cliente. * @param {number} reasonCode - Código numérico del motivo de la revocación. * @returns {Promise} Promesa con la respuesta de la solicitud. * @throws {Error} Si el SDK no está listo. */ const requestRevocation = useCallback( (reasonCode: number) => { if (state.status !== 'ready') { throw new Error( 'El SDK no está listo. No se puede llamar a requestRevocation.' ); } return apacuanaCore.requestRevocation({ reasonCode }); }, [state.status] ); /** * Obtiene la lista de motivos de revocación de certificados. * @returns {Promise} Promesa con la lista de motivos. * @throws {Error} Si el SDK no está listo. */ const getRevocationReasons = useCallback(async () => { if (state.status !== 'ready') { throw new Error( 'El SDK no está listo. No se puede llamar a getRevocationReasons.' ); } return apacuanaCore.getRevocationReasons(); }, [state.status]); /** * Crea un usuario Apacuana en la plataforma. * @param {UserData} userData - Datos del usuario a crear. * @returns {Promise} Promesa con la respuesta de la API. * @throws {Error} Si el SDK no está listo. */ const createApacuanaUser = useCallback( async (userData: UserData) => { if (state.status !== 'ready') { throw new Error( 'El SDK no está listo. No se puede llamar a createApacuanaUser.' ); } return apacuanaCore.createApacuanaUser(userData); }, [state.status] ); // /** // * Solicitud de un certificado digital. // * @param {CertificateRequestParams} params - Parámetros necesarios para la solicitud del certificado. // * @returns {Promise} Una promesa que resuelve con la respuesta de la API. // */ // const requestCertificate = useCallback( // (params: CertificateRequestParams) => { // if (state.status !== 'ready') { // throw new Error( // 'El SDK no está listo. No se puede llamar a requestRevocation.' // ); // } // console.log(params); // return apacuanaCore.requestCertificate(params); // }, // [state.status] // ); /** * Hook de efecto que maneja la inicialización del SDK Core. * Se ejecuta cuando las propiedades de configuración clave cambian. */ useEffect(() => { const initialize = async () => { setState(() => ({ status: 'initializing', error: null })); try { console.log('Iniciando SDK Core desde el Provider...', config); const initConfig = { ...config }; const configToInit = initConfig?.customerId ? initConfig : (({ customerId: _customerId, ...rest }) => rest)(initConfig); if (!initConfig.customerId) { console.log( 'No se proporcionó customerId. Se eliminará de la configuración de inicialización.' ); } await apacuanaCore.init(configToInit); console.log('SDK Core inicializado correctamente desde el Provider'); setState(() => ({ status: 'ready', error: null })); } catch (error) { console.error('Error al inicializar el SDK Core:', error); setState(() => ({ status: 'error', error: error as Error })); apacuanaCore.close(); } }; if (config.apiUrl && config.apiKey) { initialize(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [config.apiUrl, config.apiKey, config.secretKey, config.customerId]); const value: ApacuanaSDKContextValue = useMemo( () => ({ state, getCustomer, getConfig, getDocsByCustomer, getCertStatus, getRevocationReasons, generateCert, requestRevocation, signDocument, startLivenessCheck, uploadSignatureVariant, getSignatureVariant, deleteSignatureVariant, getCertTypes, getRequerimentsByTypeUser, addSigner, // requestCertificate, validateFaceLiveness, isCertificateInDevice, createApacuanaUser, }), [ state, getCustomer, getConfig, getDocsByCustomer, getCertStatus, getRevocationReasons, generateCert, requestRevocation, signDocument, startLivenessCheck, uploadSignatureVariant, getSignatureVariant, deleteSignatureVariant, getCertTypes, getRequerimentsByTypeUser, addSigner, // requestCertificate, validateFaceLiveness, isCertificateInDevice, createApacuanaUser, ] ); return ( {children} {modalContent} ); }; /** * Hook personalizado para consumir el contexto del SDK de Apacuana. * Proporciona una forma sencilla y segura de acceder al estado y las funciones del SDK. * @returns {ApacuanaSDKContextValue} El valor del contexto, que incluye el estado y las funciones del SDK. * @throws {Error} Si el hook se utiliza fuera de un `ApacuanaProvider`. */ export const useApacuana = (): ApacuanaSDKContextValue => { const context = useContext(ApacuanaContext); // Esta validación asegura que el hook se use correctamente dentro del árbol de componentes. if (context === undefined) { throw new Error( 'useApacuana debe ser utilizado dentro de un ApacuanaProvider' ); } return context; };