package com.pagseguroplugpag import br.com.uol.pagseguro.plugpagservice.wrapper.PlugPag import br.com.uol.pagseguro.plugpagservice.wrapper.PlugPagActivationData import br.com.uol.pagseguro.plugpagservice.wrapper.PlugPagCustomPrinterLayout import br.com.uol.pagseguro.plugpagservice.wrapper.PlugPagEventData import br.com.uol.pagseguro.plugpagservice.wrapper.PlugPagEventListener import br.com.uol.pagseguro.plugpagservice.wrapper.PlugPagInitializationResult import br.com.uol.pagseguro.plugpagservice.wrapper.PlugPagInstallment import br.com.uol.pagseguro.plugpagservice.wrapper.PlugPagPaymentData import br.com.uol.pagseguro.plugpagservice.wrapper.PlugPagVoidData import br.com.uol.pagseguro.plugpagservice.wrapper.PlugPagPrinterData import br.com.uol.pagseguro.plugpagservice.wrapper.PlugPagPrinterListener import br.com.uol.pagseguro.plugpagservice.wrapper.PlugPagPrintResult import br.com.uol.pagseguro.plugpagservice.wrapper.PlugPagTransactionResult import br.com.uol.pagseguro.plugpagservice.wrapper.exception.PlugPagException import br.com.uol.pagseguro.plugpagservice.wrapper.listeners.PlugPagAbortListener import br.com.uol.pagseguro.plugpagservice.wrapper.listeners.PlugPagActivationListener import br.com.uol.pagseguro.plugpagservice.wrapper.listeners.PlugPagIsActivatedListener import br.com.uol.pagseguro.plugpagservice.wrapper.listeners.PlugPagPaymentListener import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.UiThreadUtil import com.facebook.react.bridge.WritableNativeMap import com.facebook.react.modules.core.DeviceEventManagerModule import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class PagseguroPlugpagModule(reactContext: ReactApplicationContext) : NativePagseguroPlugpagSpec(reactContext) { // SDK wrapper 1.35.0: PlugPag(Context) — AppIdentification é extraída internamente pelo SDK. private val plugPag: PlugPag by lazy { PlugPag(reactApplicationContext) } // --- Helpers for activation (feature/002) --- private fun buildSdkErrorUserInfo(sdkResult: PlugPagInitializationResult): WritableNativeMap { val map = WritableNativeMap() map.putInt("result", sdkResult.result) map.putString("errorCode", sdkResult.errorCode ?: "") map.putString( "message", sdkResult.errorMessage?.takeIf { it.isNotEmpty() } ?: "Unknown error" ) return map } private fun buildInternalErrorUserInfo(e: Exception): WritableNativeMap { val map = WritableNativeMap() map.putInt("result", -1) map.putString("errorCode", "INTERNAL_ERROR") map.putString("message", e.message ?: "Unknown error") return map } // --- Helpers for printing (feature/006) --- private fun buildPrintErrorUserInfo(result: PlugPagPrintResult): WritableNativeMap { val map = WritableNativeMap() map.putInt("result", result.result) map.putString("errorCode", result.errorCode ?: "") map.putString("message", result.message?.takeIf { it.isNotEmpty() } ?: "Unknown error") return map } // --- Helper for maxTimeShowPopup layout (feature/016) --- private fun applyMaxTimeShowPopupIfPresent(data: ReadableMap) { if (data.hasKey("maxTimeShowPopup")) { val layout = PlugPagCustomPrinterLayout() layout.maxTimeShowPopup = data.getInt("maxTimeShowPopup") plugPag.setPlugPagCustomPrinterLayout(layout) } } // --- Helpers for payment (feature/003) --- private fun buildTransactionResultMap(result: PlugPagTransactionResult): WritableNativeMap { val map = WritableNativeMap() fun putStringOrNull(key: String, value: String?) { if (value != null) map.putString(key, value) else map.putNull(key) } putStringOrNull("transactionCode", result.transactionCode) putStringOrNull("transactionId", result.transactionId) putStringOrNull("date", result.date) putStringOrNull("time", result.time) putStringOrNull("hostNsu", result.hostNsu) putStringOrNull("cardBrand", result.cardBrand) putStringOrNull("bin", result.bin) putStringOrNull("holder", result.holder) putStringOrNull("userReference", result.userReference) putStringOrNull("terminalSerialNumber", result.terminalSerialNumber) putStringOrNull("amount", result.amount) putStringOrNull("availableBalance", result.availableBalance) putStringOrNull("nsu", result.nsu) putStringOrNull("cardApplication", result.cardApplication) putStringOrNull("label", result.label) putStringOrNull("holderName", result.holderName) putStringOrNull("extendedHolderName", result.extendedHolderName) putStringOrNull("autoCode", result.autoCode) return map } private fun buildSdkPaymentErrorUserInfo(result: PlugPagTransactionResult): WritableNativeMap { val map = WritableNativeMap() // PlugPagTransactionResult.result é java.lang.Integer (Int?) — pode ser null em terminais // onde o SDK não conseguiu completar a transação. Sentinela -1 é consistente com buildInternalErrorUserInfo. map.putInt("result", result.result ?: -1) map.putString("errorCode", result.errorCode ?: "") map.putString("message", result.message?.takeIf { it.isNotEmpty() } ?: "Unknown error") return map } private fun emitPaymentProgress(eventData: PlugPagEventData) { val params = Arguments.createMap() params.putInt("eventCode", eventData.eventCode) val msg = eventData.customMessage if (msg != null) params.putString("customMessage", msg) else params.putNull("customMessage") reactApplicationContext .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) .emit("onPaymentProgress", params) } // --- Helper for calculateInstallments error (feature/017) --- private fun buildInstallmentsErrorUserInfo(e: PlugPagException): WritableNativeMap { val map = WritableNativeMap() map.putInt("result", -1) map.putString("errorCode", e.errorCode ?: "") map.putString("message", e.message ?: "Unknown error") return map } // --- calculateInstallments (feature/017) --- override fun calculateInstallments(data: ReadableMap, promise: Promise) { // Threading Policy (Constituição VI): SDK calculateInstallments é bloqueante por IPC — Dispatchers.IO é necessário CoroutineScope(Dispatchers.IO).launch { try { val installmentType = when (data.getString("installmentType")) { "A_VISTA" -> PlugPag.INSTALLMENT_TYPE_A_VISTA "PARC_VENDEDOR" -> PlugPag.INSTALLMENT_TYPE_PARC_VENDEDOR "PARC_COMPRADOR" -> PlugPag.INSTALLMENT_TYPE_PARC_COMPRADOR else -> PlugPag.INSTALLMENT_TYPE_A_VISTA } val saleValue = data.getInt("amount").toString() val installments: List = plugPag.calculateInstallments(saleValue, installmentType) ?: emptyList() val optionsArray = Arguments.createArray() for (installment in installments) { val item = WritableNativeMap() item.putInt("quantity", installment.quantity) item.putInt("amount", installment.amount) item.putInt("total", installment.total) optionsArray.pushMap(item) } val resultMap = WritableNativeMap() resultMap.putArray("options", optionsArray) withContext(Dispatchers.Main) { promise.resolve(resultMap) } } catch (e: PlugPagException) { withContext(Dispatchers.Main) { promise.reject("PLUGPAG_INSTALLMENTS_ERROR", buildInstallmentsErrorUserInfo(e)) } } catch (e: Exception) { withContext(Dispatchers.Main) { promise.reject("PLUGPAG_INTERNAL_ERROR", buildInternalErrorUserInfo(e)) } } } } // --- Abort methods (feature/012) --- override fun abort(promise: Promise) { // Threading Policy (Constituição VI): SDK abort() é bloqueante por IPC — Dispatchers.IO é necessário CoroutineScope(Dispatchers.IO).launch { try { val result = plugPag.abort() withContext(Dispatchers.Main) { if (result.result != PlugPag.RET_OK) { val map = WritableNativeMap() map.putInt("result", result.result) map.putString("errorCode", "ABORT_FAILED") map.putString("message", "Abort failed with result: ${result.result}") promise.reject("PLUGPAG_ABORT_ERROR", map) } else { val successMap = WritableNativeMap() successMap.putString("result", "ok") promise.resolve(successMap) } } } catch (e: Exception) { withContext(Dispatchers.Main) { promise.reject("PLUGPAG_INTERNAL_ERROR", buildInternalErrorUserInfo(e)) } } } } override fun doAsyncAbort(promise: Promise) { // Threading Policy (Constituição VI): callbacks RxJava do SDK exigem Looper ativo na main // thread, ausente nas threads de background do TurboModule na New Arch (Issue #13). UiThreadUtil.runOnUiThread { try { plugPag.asyncAbort(object : PlugPagAbortListener { override fun onAbortRequested(abortRequested: Boolean) { if (abortRequested) { val successMap = WritableNativeMap() successMap.putString("result", "ok") promise.resolve(successMap) } else { val map = WritableNativeMap() map.putInt("result", -1) map.putString("errorCode", "ABORT_NOT_REQUESTED") map.putString("message", "Abort was not acknowledged by the terminal") promise.reject("PLUGPAG_ABORT_ERROR", map) } } override fun onError(errorMessage: String) { val map = WritableNativeMap() map.putInt("result", -1) map.putString("errorCode", "ABORT_ERROR") map.putString("message", errorMessage) promise.reject("PLUGPAG_ABORT_ERROR", map) } }) } catch (e: Exception) { promise.reject("PLUGPAG_INTERNAL_ERROR", buildInternalErrorUserInfo(e)) } } } // --- NativeEventEmitter contract (feature/003) --- override fun addListener(eventName: String) {} override fun removeListeners(count: Double) {} // --- Activation methods (feature/002, feature/019) --- override fun isAuthenticated(promise: Promise) { // Threading Policy (Constituição VI): SDK isAuthenticated é bloqueante por IPC — Dispatchers.IO é necessário CoroutineScope(Dispatchers.IO).launch { try { val authenticated = plugPag.isAuthenticated() withContext(Dispatchers.Main) { promise.resolve(authenticated) } } catch (e: Exception) { withContext(Dispatchers.Main) { promise.reject("PLUGPAG_INTERNAL_ERROR", buildInternalErrorUserInfo(e)) } } } } override fun asyncIsAuthenticated(promise: Promise) { // Threading Policy (Constituição VI): callbacks RxJava do SDK exigem Looper ativo na main // thread, ausente nas threads de background do TurboModule na New Arch (Issue #13). UiThreadUtil.runOnUiThread { try { plugPag.asyncIsAuthenticated(object : PlugPagIsActivatedListener { override fun onIsActivated(isActivated: Boolean) { promise.resolve(isActivated) } override fun onError(errorMessage: String) { val map = WritableNativeMap() map.putInt("result", -1) map.putString("errorCode", "AUTHENTICATION_ERROR") map.putString("message", errorMessage) promise.reject("PLUGPAG_AUTHENTICATION_ERROR", map) } }) } catch (e: Exception) { promise.reject("PLUGPAG_INTERNAL_ERROR", buildInternalErrorUserInfo(e)) } } } override fun initializeAndActivatePinPad(activationCode: String, promise: Promise) { // Threading Policy (Constituição VI): SDK initializeAndActivatePinpad é bloqueante por IPC — Dispatchers.IO é necessário CoroutineScope(Dispatchers.IO).launch { try { val activationData = PlugPagActivationData(activationCode) val result = plugPag.initializeAndActivatePinpad(activationData) withContext(Dispatchers.Main) { if (result.result != PlugPag.RET_OK) { promise.reject("PLUGPAG_INITIALIZATION_ERROR", buildSdkErrorUserInfo(result)) } else { val successMap = WritableNativeMap() successMap.putString("result", "ok") promise.resolve(successMap) } } } catch (e: Exception) { withContext(Dispatchers.Main) { promise.reject("PLUGPAG_INTERNAL_ERROR", buildInternalErrorUserInfo(e)) } } } } override fun doAsyncInitializeAndActivatePinPad(activationCode: String, promise: Promise) { // Threading Policy (Constituição VI): callbacks RxJava do SDK exigem Looper ativo na main // thread, ausente nas threads de background do TurboModule na New Arch (Issue #13). UiThreadUtil.runOnUiThread { try { val activationData = PlugPagActivationData(activationCode) plugPag.doAsyncInitializeAndActivatePinpad( activationData, object : PlugPagActivationListener { override fun onActivationProgress(eventData: PlugPagEventData) { // Reserved for future progress event feature } override fun onSuccess(result: PlugPagInitializationResult) { val successMap = WritableNativeMap() successMap.putString("result", "ok") promise.resolve(successMap) } override fun onError(result: PlugPagInitializationResult) { promise.reject("PLUGPAG_INITIALIZATION_ERROR", buildSdkErrorUserInfo(result)) } } ) } catch (e: Exception) { promise.reject("PLUGPAG_INTERNAL_ERROR", buildInternalErrorUserInfo(e)) } } } // --- Payment methods (feature/003) --- override fun doPayment(data: ReadableMap, promise: Promise) { // Threading Policy (Constituição VI): SDK doPayment é bloqueante por IPC — Dispatchers.IO é necessário CoroutineScope(Dispatchers.IO).launch { try { val type = when (data.getString("type")) { "CREDIT" -> PlugPag.TYPE_CREDITO "DEBIT" -> PlugPag.TYPE_DEBITO "PIX" -> PlugPag.TYPE_PIX else -> PlugPag.TYPE_CREDITO } val installmentType = when (data.getString("installmentType")) { "A_VISTA" -> PlugPag.INSTALLMENT_TYPE_A_VISTA "PARC_VENDEDOR" -> PlugPag.INSTALLMENT_TYPE_PARC_VENDEDOR "PARC_COMPRADOR" -> PlugPag.INSTALLMENT_TYPE_PARC_COMPRADOR else -> PlugPag.INSTALLMENT_TYPE_A_VISTA } val paymentData = PlugPagPaymentData( type = type, amount = data.getInt("amount"), installmentType = installmentType, installments = data.getInt("installments"), userReference = if (data.hasKey("userReference")) data.getString("userReference") else null, printReceipt = if (data.hasKey("printReceipt")) data.getBoolean("printReceipt") else false ) plugPag.setEventListener(object : PlugPagEventListener { override fun onEvent(eventData: PlugPagEventData) { emitPaymentProgress(eventData) } }) applyMaxTimeShowPopupIfPresent(data) val result = plugPag.doPayment(paymentData) withContext(Dispatchers.Main) { if (result.result != PlugPag.RET_OK) { promise.reject("PLUGPAG_PAYMENT_ERROR", buildSdkPaymentErrorUserInfo(result)) } else { promise.resolve(buildTransactionResultMap(result)) } } } catch (e: Exception) { withContext(Dispatchers.Main) { promise.reject("PLUGPAG_INTERNAL_ERROR", buildInternalErrorUserInfo(e)) } } finally { plugPag.setEventListener(object : PlugPagEventListener { override fun onEvent(eventData: PlugPagEventData) {} }) } } } override fun doAsyncPayment(data: ReadableMap, promise: Promise) { // Threading Policy (Constituição VI): métodos async baseados em listener RxJava do SDK // DEVEM ser invocados na main thread — os callbacks exigem Looper ativo, ausente nas threads // de background do TurboModule na New Arch. Invocar direto descarta o callback (Issue #13). UiThreadUtil.runOnUiThread { try { val type = when (data.getString("type")) { "CREDIT" -> PlugPag.TYPE_CREDITO "DEBIT" -> PlugPag.TYPE_DEBITO "PIX" -> PlugPag.TYPE_PIX else -> PlugPag.TYPE_CREDITO } val installmentType = when (data.getString("installmentType")) { "A_VISTA" -> PlugPag.INSTALLMENT_TYPE_A_VISTA "PARC_VENDEDOR" -> PlugPag.INSTALLMENT_TYPE_PARC_VENDEDOR "PARC_COMPRADOR" -> PlugPag.INSTALLMENT_TYPE_PARC_COMPRADOR else -> PlugPag.INSTALLMENT_TYPE_A_VISTA } val paymentData = PlugPagPaymentData( type = type, amount = data.getInt("amount"), installmentType = installmentType, installments = data.getInt("installments"), userReference = if (data.hasKey("userReference")) data.getString("userReference") else null, printReceipt = if (data.hasKey("printReceipt")) data.getBoolean("printReceipt") else false ) applyMaxTimeShowPopupIfPresent(data) plugPag.doAsyncPayment( paymentData, object : PlugPagPaymentListener { override fun onSuccess(result: PlugPagTransactionResult) { promise.resolve(buildTransactionResultMap(result)) } override fun onError(result: PlugPagTransactionResult) { promise.reject("PLUGPAG_PAYMENT_ERROR", buildSdkPaymentErrorUserInfo(result)) } override fun onPaymentProgress(eventData: PlugPagEventData) { emitPaymentProgress(eventData) } override fun onPrinterSuccess(result: PlugPagPrintResult) {} override fun onPrinterError(result: PlugPagPrintResult) {} } ) } catch (e: Exception) { promise.reject("PLUGPAG_INTERNAL_ERROR", buildInternalErrorUserInfo(e)) } } } // --- Refund methods (feature/005) --- override fun doRefund(data: ReadableMap, promise: Promise) { // Threading Policy (Constituição VI): SDK voidPayment é bloqueante por IPC — Dispatchers.IO é necessário CoroutineScope(Dispatchers.IO).launch { try { val voidType = when (data.getString("voidType")) { "VOID_PAYMENT" -> PlugPag.VOID_PAYMENT "VOID_QRCODE" -> PlugPag.VOID_QRCODE else -> PlugPag.VOID_PAYMENT } val voidData = PlugPagVoidData( transactionCode = data.getString("transactionCode")!!, transactionId = data.getString("transactionId")!!, printReceipt = if (data.hasKey("printReceipt")) data.getBoolean("printReceipt") else false, voidType = voidType ) plugPag.setEventListener(object : PlugPagEventListener { override fun onEvent(eventData: PlugPagEventData) { emitPaymentProgress(eventData) } }) applyMaxTimeShowPopupIfPresent(data) val result = plugPag.voidPayment(voidData) withContext(Dispatchers.Main) { if (result.result != PlugPag.RET_OK) { promise.reject("PLUGPAG_REFUND_ERROR", buildSdkPaymentErrorUserInfo(result)) } else { promise.resolve(buildTransactionResultMap(result)) } } } catch (e: Exception) { withContext(Dispatchers.Main) { promise.reject("PLUGPAG_INTERNAL_ERROR", buildInternalErrorUserInfo(e)) } } finally { plugPag.setEventListener(object : PlugPagEventListener { override fun onEvent(eventData: PlugPagEventData) {} }) } } } // --- Printing methods (feature/006) --- override fun printFromFile(data: ReadableMap, promise: Promise) { // Threading Policy (Constituição VI): SDK printFromFile é bloqueante por IPC — Dispatchers.IO é necessário CoroutineScope(Dispatchers.IO).launch { try { val filePath = data.getString("filePath") ?: "" val printerQuality = if (data.hasKey("printerQuality")) data.getInt("printerQuality") else 4 val steps = if (data.hasKey("steps")) data.getInt("steps") else 70 val printerData = PlugPagPrinterData(filePath, printerQuality, steps) val result = plugPag.printFromFile(printerData) withContext(Dispatchers.Main) { if (result.result != PlugPag.RET_OK) { promise.reject("PLUGPAG_PRINT_ERROR", buildPrintErrorUserInfo(result)) } else { val successMap = WritableNativeMap() successMap.putString("result", "ok") successMap.putInt("steps", result.steps) promise.resolve(successMap) } } } catch (e: Exception) { withContext(Dispatchers.Main) { promise.reject("PLUGPAG_INTERNAL_ERROR", buildInternalErrorUserInfo(e)) } } } } override fun reprintCustomerReceipt(promise: Promise) { // Threading Policy (Constituição VI): SDK reprintCustomerReceipt é bloqueante por IPC — Dispatchers.IO é necessário CoroutineScope(Dispatchers.IO).launch { try { val result = plugPag.reprintCustomerReceipt() withContext(Dispatchers.Main) { if (result.result != PlugPag.RET_OK) { promise.reject("PLUGPAG_PRINT_ERROR", buildPrintErrorUserInfo(result)) } else { val successMap = WritableNativeMap() successMap.putString("result", "ok") successMap.putInt("steps", result.steps) promise.resolve(successMap) } } } catch (e: Exception) { withContext(Dispatchers.Main) { promise.reject("PLUGPAG_INTERNAL_ERROR", buildInternalErrorUserInfo(e)) } } } } override fun doAsyncReprintCustomerReceipt(promise: Promise) { // Threading Policy (Constituição VI): callbacks RxJava do SDK exigem Looper ativo na main // thread, ausente nas threads de background do TurboModule na New Arch (Issue #13). UiThreadUtil.runOnUiThread { try { plugPag.asyncReprintCustomerReceipt(object : PlugPagPrinterListener { override fun onSuccess(result: PlugPagPrintResult) { val successMap = WritableNativeMap() successMap.putString("result", "ok") successMap.putInt("steps", result.steps) promise.resolve(successMap) } override fun onError(result: PlugPagPrintResult) { promise.reject("PLUGPAG_PRINT_ERROR", buildPrintErrorUserInfo(result)) } }) } catch (e: Exception) { promise.reject("PLUGPAG_INTERNAL_ERROR", buildInternalErrorUserInfo(e)) } } } override fun reprintEstablishmentReceipt(promise: Promise) { // Threading Policy (Constituição VI): SDK reprintStablishmentReceipt é bloqueante por IPC — Dispatchers.IO é necessário CoroutineScope(Dispatchers.IO).launch { try { // "Stablishment" is the SDK's spelling — see FR-013 val result = plugPag.reprintStablishmentReceipt() withContext(Dispatchers.Main) { if (result.result != PlugPag.RET_OK) { promise.reject("PLUGPAG_PRINT_ERROR", buildPrintErrorUserInfo(result)) } else { val successMap = WritableNativeMap() successMap.putString("result", "ok") successMap.putInt("steps", result.steps) promise.resolve(successMap) } } } catch (e: Exception) { withContext(Dispatchers.Main) { promise.reject("PLUGPAG_INTERNAL_ERROR", buildInternalErrorUserInfo(e)) } } } } override fun doAsyncReprintEstablishmentReceipt(promise: Promise) { // Threading Policy (Constituição VI): callbacks RxJava do SDK exigem Looper ativo na main // thread, ausente nas threads de background do TurboModule na New Arch (Issue #13). UiThreadUtil.runOnUiThread { try { plugPag.asyncReprintEstablishmentReceipt(object : PlugPagPrinterListener { override fun onSuccess(result: PlugPagPrintResult) { val successMap = WritableNativeMap() successMap.putString("result", "ok") successMap.putInt("steps", result.steps) promise.resolve(successMap) } override fun onError(result: PlugPagPrintResult) { promise.reject("PLUGPAG_PRINT_ERROR", buildPrintErrorUserInfo(result)) } }) } catch (e: Exception) { promise.reject("PLUGPAG_INTERNAL_ERROR", buildInternalErrorUserInfo(e)) } } } companion object { const val NAME = NativePagseguroPlugpagSpec.NAME } }