tentando passagem de context para paymentApplication

This commit is contained in:
Ighor Moura 2025-08-21 17:51:01 -04:00
parent 0abcf81d79
commit a5f0139307
7 changed files with 837 additions and 317 deletions

View File

@ -12,19 +12,29 @@ android {
applicationId = "com.example.mypos"
minSdk = 23
targetSdk = 36
versionCode = 1
versionCode = 2
versionName = "1.0"
buildConfigField ("String", "ACTIVATION_CODE", "\"\"")
buildConfigField ("String", "APPLICATION_TOKEN", "\"\"")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
signingConfigs {
create("release") {
storeFile = file("mypos.jks")
storePassword = "app644223"
keyAlias = "pos"
keyPassword = "app644223"
}
}
buildTypes {
debug {
buildConfigField ("String", "ACTIVATION_CODE", "\"436596309\"")
buildConfigField ("String", "APPLICATION_TOKEN", "\"mk_MbQ92RRHEOcGFRIf9/R1A\"")
}
release {
signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
@ -47,7 +57,8 @@ android {
}
dependencies {
implementation(files("libs/AditumSdkIntegration-2.3.4.860743-release.aar"))
// implementation(files("libs/AditumSdkIntegration-2.3.4.860743-release.aar"))
implementation(files("libs/AditumSdkIntegration-2.3.7-App2App.763915-release.aar"))
implementation("com.google.code.gson:gson:2.13.1")
implementation("androidx.datastore:datastore-preferences:1.1.7")
implementation(libs.androidx.core.ktx)

BIN
app/mypos.jks Normal file

Binary file not shown.

View File

@ -1,8 +1,8 @@
package com.example.mypos.models
import android.content.Context
import android.nfc.Tag
import android.util.Log
import androidx.datastore.preferences.core.edit
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
@ -33,13 +33,28 @@ import br.com.aditum.data.v2.model.transactions.ConfirmTransactionCallback
import br.com.aditum.data.v2.model.transactions.PendingTransactionsCallback
import com.example.mypos.BuildConfig
import com.example.mypos.data.AditumError
import com.example.mypos.data.PaymentRegister
import com.example.mypos.preferences.PrefKeys
import com.example.mypos.preferences.dataStore
import com.example.mypos.services.PaymentApplication
import com.google.gson.Gson
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
enum class SdkState {
NOT_INITIALIZED,
INITIALIZING,
IDLE,
PROCESSING,
PAYING,
CANCELING
}
class PaymentViewModel(
private val application: PaymentApplication,
@ -48,65 +63,265 @@ class PaymentViewModel(
private val paymentApplication = application
private val gson = Gson()
private val TAG = "PaymentViewModel"
// Estado do SDK
private val _sdkState = MutableStateFlow(SdkState.NOT_INITIALIZED)
val sdkState: StateFlow<SdkState> = _sdkState.asStateFlow()
// Estado de conexão do serviço
private val _isServiceConnected = MutableStateFlow(false)
val isServiceConnected: StateFlow<Boolean> = _isServiceConnected.asStateFlow()
// Mensagens e dados do PIN pad
private val _messageFlow = MutableStateFlow("")
val messageFlow: StateFlow<String> = _messageFlow.asStateFlow()
private val _displayFlow = MutableStateFlow("")
val displayFlow: StateFlow<String> = _displayFlow.asStateFlow()
private val _pinLengthFlow = MutableStateFlow(0)
val pinLengthFlow: StateFlow<Int> = _pinLengthFlow.asStateFlow()
// Estado de erro
private val _errorFlow = MutableStateFlow<Pair<String, String?>?>(null)
val errorFlow: StateFlow<Pair<String, String?>?> = _errorFlow.asStateFlow()
init {
viewModelScope.launch {
paymentApplication.isServiceConnectedFlow.collect { isConnected ->
_isServiceConnected.value = isConnected
if (isConnected) {
try {
paymentApplication.communicationService?.registerPaymentCallback(paymentCallback)
paymentApplication.communicationService?.registerPaymentCallback(notificationCallback)
?: Log.e(TAG, "Failed to register payment callback: communicationService is null")
// Tentar inicializar automaticamente quando o serviço se conectar
autoInitialize()
} catch (e: Exception) {
Log.e(TAG, "Error registering payment callback: ${e.message}")
setError(AditumError.SERVICE_NULL, e.message)
}
} else {
// Quando o serviço desconecta, voltar para NOT_INITIALIZED
if (_sdkState.value != SdkState.NOT_INITIALIZED) {
_sdkState.value = SdkState.NOT_INITIALIZED
}
}
}
}
}
fun startService() {
paymentApplication.startAditumSdkService()
}
val paymentCallback = object : IPaymentCallback.Stub() {
private val notificationCallback = object : IPaymentCallback.Stub() {
override fun notification(
message: String?,
transactionStatus: TransactionStatus?,
command: AbecsCommands?
message: String,
transactionStatus: TransactionStatus,
command: AbecsCommands
) {
Log.i(TAG, "\nnotification - ${ message ?: "" }")
if (message != null) {
_messageFlow.value = message.replace("\\s+".toRegex(), " ").trim()
val filteredMessage = message.replace("\\s+".toRegex(), " ").trim()
viewModelScope.launch {
if (command == AbecsCommands.Display && filteredMessage != "Callback Added") {
_displayFlow.emit(filteredMessage)
}
_messageFlow.emit(filteredMessage)
}
}
override fun pinNotification(message: String?, length: Int) {
_pinLengthFlow.value = length
override fun pinNotification(message: String, length: Int) {
viewModelScope.launch {
_pinLengthFlow.emit(length)
_messageFlow.emit("PIN entrada: $length dígitos")
}
}
override fun startGetClearData(
clearDataRequest: GetClearDataRequest?,
finished: GetClearDataFinishedCallback?
) {
TODO("Not yet implemented")
if (clearDataRequest != null) {
Log.d(TAG, "clearDataRequest: ${clearDataRequest.title} ${clearDataRequest.alphaNumeric} ${clearDataRequest.timeout}")
}
}
override fun startGetMenuSelection(
menuSelectionRequest: GetMenuSelectionRequest?,
clearDataRequest: GetMenuSelectionRequest?,
finished: GetMenuSelectionFinishedCallback?
) {
TODO("Not yet implemented")
// Implementação customizada se necessário
}
override fun qrCodeGenerated(qrCode: String?, expirationTime: Int) {
Log.d(TAG, "\nqrCode - ${ qrCode ?: "" }\nexpirationTime - $expirationTime")
override fun qrCodeGenerated(qrCode: String, expirationTime: Int) {
viewModelScope.launch {
_messageFlow.emit("QR Code gerado - Expira em: $expirationTime segundos")
}
}
}
private fun setError(errorCode: String, errorMessage: String?) {
_errorFlow.value = Pair(errorCode, errorMessage)
}
private fun clearError() {
_errorFlow.value = null
}
private suspend fun getActivationCode(): String? {
return context.dataStore.data.map { preferences ->
preferences[PrefKeys.ACTIVATION_CODE]
}.firstOrNull()
}
private suspend fun getNewActivationCode(): String? {
return context.dataStore.data.map { preferences ->
preferences[PrefKeys.NEW_ACTIVATION_CODE]
}.firstOrNull()
}
fun saveActivationCode(code: String?) {
viewModelScope.launch {
context.dataStore.edit { preferences ->
if (code != null) {
preferences[PrefKeys.ACTIVATION_CODE] = code
} else {
preferences.remove(PrefKeys.ACTIVATION_CODE)
}
}
}
}
fun saveNewActivationCode(code: String?) {
viewModelScope.launch {
context.dataStore.edit { preferences ->
if (code != null) {
preferences[PrefKeys.NEW_ACTIVATION_CODE] = code
} else {
preferences.remove(PrefKeys.NEW_ACTIVATION_CODE)
}
}
}
}
private suspend fun clearNewActivationCode() {
context.dataStore.edit { preferences ->
preferences.remove(PrefKeys.NEW_ACTIVATION_CODE)
}
}
private suspend fun autoInitialize() {
Log.d(TAG, "autoInitialize - Iniciando inicialização automática")
try {
val activationCode = getActivationCode()
val newActivationCode = getNewActivationCode()
Log.d(TAG, "autoInitialize - activation_code: $activationCode, new_activation_code: $newActivationCode")
if (activationCode == null && newActivationCode == null) {
Log.d(TAG, "autoInitialize - Nenhum código de ativação encontrado")
return
}
// Se os códigos são iguais, não precisa fazer nada
if (activationCode != null && newActivationCode != null && activationCode == newActivationCode) {
Log.d(TAG, "autoInitialize - Códigos são iguais, limpando new_activation_code")
clearNewActivationCode()
return
}
// Se existe um novo código diferente do atual, fazer a troca
if (newActivationCode != null && activationCode != null && activationCode != newActivationCode) {
Log.d(TAG, "autoInitialize - Desativando código antigo")
val deactivationSuccess = suspendCancellableCoroutine<Boolean> { continuation ->
deactivateInternal(
resolve = { success -> continuation.resume(success) },
reject = { error, message ->
Log.e(TAG, "Erro na desativação: $error - $message")
continuation.resume(false)
}
)
}
if (!deactivationSuccess) {
Log.e(TAG, "autoInitialize - Falha ao desativar código antigo")
setError(AditumError.DEACTIVATE_ERROR, "Falha ao desativar para usar novo código")
return
}
}
// Usar o código apropriado para inicialização
val codeToUse = newActivationCode ?: activationCode
Log.d(TAG, "autoInitialize - Inicializando com código: $codeToUse")
initAditumSdkInternal(
applicationName = "MyPOS",
applicationVersion = BuildConfig.VERSION_NAME,
activationCode = codeToUse,
resolve = { success ->
if (success && newActivationCode != null) {
// Se usou o novo código com sucesso, salvar como atual e limpar o novo
viewModelScope.launch {
saveActivationCode(newActivationCode)
clearNewActivationCode()
Log.d(TAG, "autoInitialize - Novo código salvo como atual")
}
}
},
reject = { error, message ->
Log.e(TAG, "autoInitialize - Erro na inicialização: $error - $message")
setError(error, message)
}
)
} catch (e: Exception) {
Log.e(TAG, "autoInitialize - Exceção: ${e.message}")
setError(AditumError.INIT_ERROR, e.message)
}
}
private suspend fun ensureServiceReady(): Boolean {
if (!isServiceConnected.value) {
Log.d(TAG, "ensureServiceReady - Serviço não conectado, tentando conectar")
val connected = paymentApplication.startAditumSdkService(context)
if (!connected) {
setError(AditumError.SERVICE_NOT_AVAILABLE, AditumError.SERVICE_NOT_AVAILABLE_MESSAGE)
return false
}
}
// Verificar se o SDK está em estado apropriado
when (_sdkState.value) {
SdkState.NOT_INITIALIZED -> {
Log.d(TAG, "ensureServiceReady - SDK não inicializado, tentando inicializar")
return initializeIfNeeded()
}
SdkState.INITIALIZING, SdkState.PROCESSING, SdkState.PAYING, SdkState.CANCELING -> {
setError(AditumError.SERVICE_NULL, "SDK ocupado com outra operação")
return false
}
SdkState.IDLE -> {
return true
}
}
return false
}
private suspend fun initializeIfNeeded(): Boolean = suspendCancellableCoroutine { continuation ->
val activationCode = viewModelScope.launch {
val currentCode = getActivationCode()
val newCode = getNewActivationCode()
val codeToUse = newCode ?: currentCode
if (codeToUse == null) {
setError(AditumError.INIT_ERROR, "Nenhum código de ativação disponível")
continuation.resume(false)
return@launch
}
initAditumSdkInternal(
applicationName = "MyPOS",
applicationVersion = BuildConfig.VERSION_NAME,
activationCode = codeToUse,
resolve = { success -> continuation.resume(success) },
reject = { error, message ->
setError(error, message)
continuation.resume(false)
}
)
}
}
@ -116,13 +331,35 @@ class PaymentViewModel(
activationCode: String?,
resolve: (Boolean) -> Unit = {},
reject: (String, String?) -> Unit = { _, _ -> }
) {
if (_sdkState.value == SdkState.INITIALIZING) {
reject(AditumError.INIT_ERROR, "SDK já está inicializando")
return
}
initAditumSdkInternal(applicationName, applicationVersion, activationCode, resolve, reject)
}
private fun initAditumSdkInternal(
applicationName: String,
applicationVersion: String,
activationCode: String?,
resolve: (Boolean) -> Unit = {},
reject: (String, String?) -> Unit = { _, _ -> }
) {
viewModelScope.launch {
try {
if (!paymentApplication.ensureServiceConnected()) {
_sdkState.value = SdkState.INITIALIZING
clearError()
if (!isServiceConnected.value) {
val connected = paymentApplication.startAditumSdkService(context)
if (!connected) {
_sdkState.value = SdkState.NOT_INITIALIZED
reject(AditumError.SERVICE_NOT_AVAILABLE, AditumError.SERVICE_NOT_AVAILABLE_MESSAGE)
return@launch
}
}
val pinpadMessages = PinpadMessages().apply {
mainMessage = applicationName
@ -139,43 +376,37 @@ class PaymentViewModel(
val callback = object : InitResponseCallback.Stub() {
override fun onResponse(initResponse: InitResponse?) {
if (initResponse != null) {
resolve(initResponse.initialized)
viewModelScope.launch {
if (initResponse != null && initResponse.initialized) {
_sdkState.value = SdkState.IDLE
if (activationCode != null) {
saveActivationCode(activationCode)
clearNewActivationCode()
}
resolve(true)
} else {
Log.e(TAG, "onResponse - initResponse is null")
reject(AditumError.INIT_RESPONSE_NULL, AditumError.INIT_RESPONSE_NULL_MESSAGE)
_sdkState.value = SdkState.NOT_INITIALIZED
val errorMsg = "Falha na inicialização do SDK"
setError(AditumError.INIT_ERROR, errorMsg)
reject(AditumError.INIT_ERROR, errorMsg)
}
}
}
}
paymentApplication.communicationService?.init(initRequest, callback)
?: reject(AditumError.SERVICE_NULL, AditumError.SERVICE_NULL_MESSAGE)
?: run {
_sdkState.value = SdkState.NOT_INITIALIZED
reject(AditumError.SERVICE_NULL, AditumError.SERVICE_NULL_MESSAGE)
}
} catch (e: Exception) {
_sdkState.value = SdkState.NOT_INITIALIZED
setError(AditumError.INIT_ERROR, "${AditumError.INIT_ERROR_MESSAGE}: ${e.message}")
reject(AditumError.INIT_ERROR, "${AditumError.INIT_ERROR_MESSAGE}: ${e.message}")
}
}
}
fun getMerchantData(resolve: (MerchantData) -> Unit = {}, reject: (String, String?) -> Unit = { _, _ -> }) {
viewModelScope.launch {
try {
if (!paymentApplication.ensureServiceConnected()) {
reject(AditumError.SERVICE_NOT_AVAILABLE, AditumError.SERVICE_NOT_AVAILABLE_MESSAGE)
return@launch
}
val merchantData: MerchantData? = paymentApplication.communicationService?.merchantData
if (merchantData != null) {
resolve(merchantData)
} else {
reject(AditumError.MERCHANT_DATA_NULL, AditumError.MERCHANT_DATA_NULL_MESSAGE)
}
} catch (e: Exception) {
reject(AditumError.MERCHANT_DATA_ERROR, "${AditumError.MERCHANT_DATA_ERROR_MESSAGE}: ${e.message}")
}
}
}
fun pay(
amount: Long,
installments: Int? = null,
@ -187,13 +418,17 @@ class PaymentViewModel(
) {
viewModelScope.launch {
try {
if (!paymentApplication.ensureServiceConnected()) {
reject(AditumError.SERVICE_NOT_AVAILABLE, AditumError.SERVICE_NOT_AVAILABLE_MESSAGE)
if (!ensureServiceReady()) {
return@launch
}
_sdkState.value = SdkState.PAYING
clearError()
val adjustedAmount = amountSeasoning?.invoke(amount) ?: amount
if (adjustedAmount <= 0) {
_sdkState.value = SdkState.IDLE
setError(AditumError.INVALID_AMOUNT, AditumError.INVALID_AMOUNT_MESSAGE)
reject(AditumError.INVALID_AMOUNT, AditumError.INVALID_AMOUNT_MESSAGE)
return@launch
}
@ -216,20 +451,30 @@ class PaymentViewModel(
val callback = object : PaymentResponseCallback.Stub() {
override fun onResponse(paymentResponse: PaymentResponse?) {
viewModelScope.launch {
_sdkState.value = SdkState.IDLE
if (paymentResponse != null) {
val json = gson.toJson(paymentResponse)
Log.d(TAG, "onResponse - $json")
Log.d(TAG, "Payment response: $json")
resolve(paymentResponse)
} else {
Log.e(TAG, "onResponse - paymentResponse is null")
reject(AditumError.PAYMENT_RESPONSE_NULL, AditumError.PAYMENT_RESPONSE_NULL_MESSAGE)
val errorMsg = "Resposta de pagamento nula"
setError(AditumError.PAYMENT_RESPONSE_NULL, errorMsg)
reject(AditumError.PAYMENT_RESPONSE_NULL, errorMsg)
}
}
}
}
paymentApplication.communicationService?.pay(paymentRequest, callback)
?: reject(AditumError.SERVICE_NULL, AditumError.SERVICE_NULL_MESSAGE)
?: run {
_sdkState.value = SdkState.IDLE
setError(AditumError.SERVICE_NULL, AditumError.SERVICE_NULL_MESSAGE)
reject(AditumError.SERVICE_NULL, AditumError.SERVICE_NULL_MESSAGE)
}
} catch (e: Exception) {
_sdkState.value = SdkState.IDLE
setError(AditumError.PAYMENT_ERROR, "${AditumError.PAYMENT_ERROR_MESSAGE}: ${e.message}")
reject(AditumError.PAYMENT_ERROR, "${AditumError.PAYMENT_ERROR_MESSAGE}: ${e.message}")
}
}
@ -238,20 +483,31 @@ class PaymentViewModel(
fun confirm(nsu: String, resolve: (Boolean) -> Unit = {}, reject: (String, String?) -> Unit = { _, _ -> }) {
viewModelScope.launch {
try {
if (!paymentApplication.ensureServiceConnected()) {
reject(AditumError.SERVICE_NOT_AVAILABLE, AditumError.SERVICE_NOT_AVAILABLE_MESSAGE)
if (!ensureServiceReady()) {
return@launch
}
_sdkState.value = SdkState.PROCESSING
clearError()
val callback = object : ConfirmTransactionCallback.Stub() {
override fun onResponse(confirmed: Boolean) {
viewModelScope.launch {
_sdkState.value = SdkState.IDLE
resolve(confirmed)
}
}
}
paymentApplication.communicationService?.confirmTransaction(nsu, callback)
?: reject(AditumError.SERVICE_NULL, AditumError.SERVICE_NULL_MESSAGE)
?: run {
_sdkState.value = SdkState.IDLE
setError(AditumError.SERVICE_NULL, AditumError.SERVICE_NULL_MESSAGE)
reject(AditumError.SERVICE_NULL, AditumError.SERVICE_NULL_MESSAGE)
}
} catch (e: Exception) {
_sdkState.value = SdkState.IDLE
setError(AditumError.CONFIRM_ERROR, "${AditumError.CONFIRM_ERROR_MESSAGE}: ${e.message}")
reject(AditumError.CONFIRM_ERROR, "${AditumError.CONFIRM_ERROR_MESSAGE}: ${e.message}")
}
}
@ -260,91 +516,152 @@ class PaymentViewModel(
fun cancel(nsu: String, isReversal: Boolean, resolve: (Boolean) -> Unit = {}, reject: (String, String?) -> Unit = { _, _ -> }) {
viewModelScope.launch {
try {
if (!paymentApplication.ensureServiceConnected()) {
reject(AditumError.SERVICE_NOT_AVAILABLE, AditumError.SERVICE_NOT_AVAILABLE_MESSAGE)
if (!ensureServiceReady()) {
return@launch
}
_sdkState.value = SdkState.CANCELING
clearError()
val cancelationRequest = CancelationRequest(nsu, isReversal)
val callback = object : CancelationResponseCallback.Stub() {
override fun onResponse(cancelationResponse: CancelationResponse?) {
viewModelScope.launch {
_sdkState.value = SdkState.IDLE
if (cancelationResponse != null) {
resolve(cancelationResponse.canceled)
} else {
Log.e(TAG, "onResponse - cancelationResponse is null")
reject(AditumError.CANCEL_RESPONSE_NULL, AditumError.CANCEL_RESPONSE_NULL_MESSAGE)
val errorMsg = "Resposta de cancelamento nula"
setError(AditumError.CANCEL_RESPONSE_NULL, errorMsg)
reject(AditumError.CANCEL_RESPONSE_NULL, errorMsg)
}
}
}
}
paymentApplication.communicationService?.cancel(cancelationRequest, callback)
?: reject(AditumError.SERVICE_NULL, AditumError.SERVICE_NULL_MESSAGE)
?: run {
_sdkState.value = SdkState.IDLE
setError(AditumError.SERVICE_NULL, AditumError.SERVICE_NULL_MESSAGE)
reject(AditumError.SERVICE_NULL, AditumError.SERVICE_NULL_MESSAGE)
}
} catch (e: Exception) {
_sdkState.value = SdkState.IDLE
setError(AditumError.CANCEL_ERROR, "${AditumError.CANCEL_ERROR_MESSAGE}: ${e.message}")
reject(AditumError.CANCEL_ERROR, "${AditumError.CANCEL_ERROR_MESSAGE}: ${e.message}")
}
}
}
fun aboutOperation(resolve: (Boolean) -> Unit = {}, reject: (String, String?) -> Unit = { _, _ -> }) {
fun abortOperation(resolve: (Boolean) -> Unit = {}, reject: (String, String?) -> Unit = { _, _ -> }) {
viewModelScope.launch {
if (!paymentApplication.ensureServiceConnected()) {
reject(AditumError.SERVICE_NOT_AVAILABLE, AditumError.SERVICE_NOT_AVAILABLE_MESSAGE)
try {
if (!ensureServiceReady()) {
return@launch
}
paymentApplication.communicationService?.abortOperation()
_sdkState.value = SdkState.IDLE
resolve(true)
} catch (e: Exception) {
setError(AditumError.SERVICE_NULL, "${AditumError.SERVICE_NULL_MESSAGE}: ${e.message}")
reject(AditumError.SERVICE_NULL, "${AditumError.SERVICE_NULL_MESSAGE}: ${e.message}")
}
}
}
fun pendingTransactions(resolve: (List<Charge?>?) -> Unit = {}, reject: (String, String?) -> Unit = { _, _ ->}) {
fun pendingTransactions(resolve: (List<Charge?>?) -> Unit = {}, reject: (String, String?) -> Unit = { _, _ -> }) {
viewModelScope.launch {
try {
if (!paymentApplication.ensureServiceConnected()) {
reject(AditumError.SERVICE_NOT_AVAILABLE, AditumError.SERVICE_NOT_AVAILABLE_MESSAGE)
if (!ensureServiceReady()) {
return@launch
}
_sdkState.value = SdkState.PROCESSING
val callback = object : PendingTransactionsCallback.Stub() {
override fun onResponse(results: List<Charge?>?) {
viewModelScope.launch {
_sdkState.value = SdkState.IDLE
val json = gson.toJson(results)
Log.e(TAG, "onResponse - $json")
Log.d(TAG, "Pending transactions: $json")
resolve(results)
}
}
}
paymentApplication.communicationService?.pendingTransactions(callback)
?: reject(AditumError.SERVICE_NULL, AditumError.SERVICE_NULL_MESSAGE)
?: run {
_sdkState.value = SdkState.IDLE
setError(AditumError.SERVICE_NULL, AditumError.SERVICE_NULL_MESSAGE)
reject(AditumError.SERVICE_NULL, AditumError.SERVICE_NULL_MESSAGE)
}
} catch (e: Exception) {
_sdkState.value = SdkState.IDLE
setError(AditumError.CONFIRM_ERROR, "${AditumError.CONFIRM_ERROR_MESSAGE}: ${e.message}")
reject(AditumError.CONFIRM_ERROR, "${AditumError.CONFIRM_ERROR_MESSAGE}: ${e.message}")
}
}
}
fun deactivate(resolve: (Boolean) -> Unit = {}, reject: (String, String?) -> Unit = { _, _ -> }) {
deactivateInternal(resolve, reject)
}
private fun deactivateInternal(resolve: (Boolean) -> Unit = {}, reject: (String, String?) -> Unit = { _, _ -> }) {
viewModelScope.launch {
try {
if (!paymentApplication.ensureServiceConnected()) {
if (!isServiceConnected.value) {
reject(AditumError.SERVICE_NOT_AVAILABLE, AditumError.SERVICE_NOT_AVAILABLE_MESSAGE)
return@launch
}
val callback = object : DeactivationResponseCallback.Stub() {
override fun onResponse(status: Boolean) {
Log.d(TAG, "onDeactivationResponse - deactivationResponse: $status")
viewModelScope.launch {
_sdkState.value = SdkState.NOT_INITIALIZED
Log.d(TAG, "Deactivation response: $status")
if (status) {
// Limpar os códigos de ativação quando desativar
saveActivationCode(null)
clearNewActivationCode()
}
resolve(status)
}
}
}
paymentApplication.communicationService?.deactivate(callback)
?: reject(AditumError.SERVICE_NULL, AditumError.SERVICE_NULL_MESSAGE)
} catch (e: Exception) {
setError(AditumError.DEACTIVATE_ERROR, "${AditumError.DEACTIVATE_ERROR_MESSAGE}: ${e.message}")
reject(AditumError.DEACTIVATE_ERROR, "${AditumError.DEACTIVATE_ERROR_MESSAGE}: ${e.message}")
}
}
}
fun getMerchantData(resolve: (MerchantData) -> Unit = {}, reject: (String, String?) -> Unit = { _, _ -> }) {
viewModelScope.launch {
try {
if (!ensureServiceReady()) {
return@launch
}
val merchantData: MerchantData? = paymentApplication.communicationService?.merchantData
if (merchantData != null) {
resolve(merchantData)
} else {
setError(AditumError.MERCHANT_DATA_NULL, AditumError.MERCHANT_DATA_NULL_MESSAGE)
reject(AditumError.MERCHANT_DATA_NULL, AditumError.MERCHANT_DATA_NULL_MESSAGE)
}
} catch (e: Exception) {
setError(AditumError.MERCHANT_DATA_ERROR, "${AditumError.MERCHANT_DATA_ERROR_MESSAGE}: ${e.message}")
reject(AditumError.MERCHANT_DATA_ERROR, "${AditumError.MERCHANT_DATA_ERROR_MESSAGE}: ${e.message}")
}
}
}
companion object {
fun provideFactory(
paymentApplication: PaymentApplication,

View File

@ -11,4 +11,5 @@ val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "my
object PrefKeys {
val SERIAL_NUMBER = stringPreferencesKey("session_serial_number")
val ACTIVATION_CODE = stringPreferencesKey("session_activation_code")
val NEW_ACTIVATION_CODE = stringPreferencesKey("new_session_activation_code")
}

View File

@ -1,21 +1,26 @@
package com.example.mypos.screen
import android.os.Handler
import android.os.Looper
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import br.com.aditum.data.v2.enums.PaymentType
import com.example.mypos.BuildConfig
import com.example.mypos.models.PaymentViewModel
import kotlinx.coroutines.flow.collectLatest
import com.example.mypos.models.SdkState
@Composable
fun PaymentScreen(viewModel: PaymentViewModel) {
@ -23,99 +28,242 @@ fun PaymentScreen(viewModel: PaymentViewModel) {
var amount by remember { mutableStateOf("") }
var installments by remember { mutableStateOf("1") }
var paymentType by remember { mutableStateOf(PaymentType.Credit) }
var activationCode by remember { mutableStateOf(if (BuildConfig.DEBUG) BuildConfig.ACTIVATION_CODE else "") }
var newActivationCode by remember { mutableStateOf(if (BuildConfig.DEBUG) BuildConfig.ACTIVATION_CODE else "") }
val isServiceConnected by viewModel.isServiceConnected.collectAsState()
val sdkState by viewModel.sdkState.collectAsState()
val message by viewModel.messageFlow.collectAsState()
val pinLength by viewModel.pinLengthFlow.collectAsState()
var isInitializing by remember { mutableStateOf(false) }
var isPaying by remember { mutableStateOf(false) }
var showLoading by remember { mutableStateOf(false) }
val error by viewModel.errorFlow.collectAsState()
val scrollState = rememberScrollState()
val handler = remember { Handler(Looper.getMainLooper()) }
if (showLoading) {
LoadingScreen(
message = message,
onDismiss = { showLoading = false }
)
LaunchedEffect(error) {
error?.let { (errorCode, errorMessage) ->
handler.post {
Toast.makeText(context, "Erro: $errorMessage", Toast.LENGTH_LONG).show()
}
}
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
.padding(16.dp)
.verticalScroll(scrollState),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = "Pagamento",
text = "Sistema de Pagamento POS",
style = MaterialTheme.typography.headlineMedium
)
Text(
text = if (isServiceConnected) "Serviço Conectado" else "Serviço Desconectado",
color = if (isServiceConnected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = when {
!isServiceConnected -> MaterialTheme.colorScheme.errorContainer
sdkState == SdkState.IDLE -> MaterialTheme.colorScheme.primaryContainer
sdkState == SdkState.NOT_INITIALIZED -> MaterialTheme.colorScheme.secondaryContainer
else -> MaterialTheme.colorScheme.tertiaryContainer
}
)
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Status do Sistema",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = if (isServiceConnected) "Serviço: Conectado" else "Serviço: Desconectado",
style = MaterialTheme.typography.bodyMedium
)
Text(
text = "SDK: ${getSdkStateText(sdkState)}",
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium
)
if (message.isNotEmpty()) {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = message,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
if (pinLength > 0) {
Text(
text = "PIN: ${"*".repeat(pinLength)}",
style = MaterialTheme.typography.bodySmall,
fontWeight = FontWeight.Bold
)
}
}
}
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Códigos de Ativação",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = activationCode,
onValueChange = { activationCode = it },
label = { Text("Código de Ativação Atual") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword),
modifier = Modifier.fillMaxWidth(),
enabled = sdkState != SdkState.INITIALIZING
)
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = {
if (!isServiceConnected) {
viewModel.startService()
if (activationCode.isBlank()) {
handler.post {
Toast.makeText(context, "Tentando conectar ao serviço...", Toast.LENGTH_SHORT).show()
Toast.makeText(context, "Código de ativação não pode estar vazio", Toast.LENGTH_SHORT).show()
}
return@Button
}
if (!isInitializing) {
isInitializing = true
showLoading = true
viewModel.saveActivationCode(activationCode)
handler.post {
Toast.makeText(context, "Código de ativação salvo com sucesso", Toast.LENGTH_SHORT).show()
}
},
enabled = sdkState != SdkState.INITIALIZING,
modifier = Modifier.fillMaxWidth()
) {
Text("Salvar Código Atual")
}
Spacer(modifier = Modifier.height(12.dp))
OutlinedTextField(
value = newActivationCode,
onValueChange = { newActivationCode = it },
label = { Text("Novo Código de Ativação") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword),
modifier = Modifier.fillMaxWidth(),
enabled = sdkState != SdkState.INITIALIZING
)
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = {
if (newActivationCode.isBlank()) {
handler.post {
Toast.makeText(context, "Novo código de ativação não pode estar vazio", Toast.LENGTH_SHORT).show()
}
return@Button
}
viewModel.saveNewActivationCode(newActivationCode)
handler.post {
Toast.makeText(context, "Novo código de ativação salvo com sucesso", Toast.LENGTH_SHORT).show()
}
},
enabled = sdkState != SdkState.INITIALIZING,
modifier = Modifier.fillMaxWidth()
) {
Text("Salvar Novo Código")
}
}
}
Button(
onClick = {
val codeToUse = if (newActivationCode.isNotBlank()) newActivationCode else activationCode
if (codeToUse.isBlank()) {
handler.post {
Toast.makeText(context, "Insira um código de ativação", Toast.LENGTH_SHORT).show()
}
return@Button
}
viewModel.initAditumSdk(
applicationName = "MyPOS",
applicationVersion = "1.0.0",
activationCode = BuildConfig.ACTIVATION_CODE,
applicationVersion = BuildConfig.VERSION_NAME,
activationCode = codeToUse,
resolve = { success ->
isInitializing = false
showLoading = false
handler.post {
Toast.makeText(
context,
if (success) "SDK inicializado com sucesso!" else "Falha na inicialização.",
if (success) "SDK inicializado com sucesso!" else "Falha na inicialização do SDK",
Toast.LENGTH_LONG
).show()
}
},
reject = { errorCode, errorMessage ->
isInitializing = false
showLoading = false
handler.post {
Toast.makeText(context, "Erro: $errorMessage", Toast.LENGTH_LONG).show()
}
}
)
}
},
enabled = !isInitializing,
enabled = sdkState !in listOf(SdkState.INITIALIZING, SdkState.PROCESSING, SdkState.PAYING, SdkState.CANCELING),
modifier = Modifier.fillMaxWidth()
) {
Text(if (isInitializing) "Inicializando..." else "Inicializar SDK")
Text(
when (sdkState) {
SdkState.INITIALIZING -> "Inicializando SDK..."
SdkState.IDLE -> "Reinicializar SDK"
else -> "Inicializar SDK"
}
)
}
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Realizar Pagamento",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = amount,
onValueChange = { amount = it.filter { char -> char.isDigit() || char == '.' } },
label = { Text("Valor (R$)") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
modifier = Modifier.fillMaxWidth()
modifier = Modifier.fillMaxWidth(),
enabled = sdkState == SdkState.IDLE
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = installments,
onValueChange = { installments = it.filter { char -> char.isDigit() } },
label = { Text("Quantidade de Parcelas") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth(),
enabled = paymentType == PaymentType.Credit
enabled = paymentType == PaymentType.Credit && sdkState == SdkState.IDLE
)
Spacer(modifier = Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
@ -123,102 +271,166 @@ fun PaymentScreen(viewModel: PaymentViewModel) {
Row(verticalAlignment = Alignment.CenterVertically) {
RadioButton(
selected = paymentType == PaymentType.Credit,
onClick = { paymentType = PaymentType.Credit }
onClick = { if (sdkState == SdkState.IDLE) paymentType = PaymentType.Credit },
enabled = sdkState == SdkState.IDLE
)
Text("Crédito")
}
Row(verticalAlignment = Alignment.CenterVertically) {
RadioButton(
selected = paymentType == PaymentType.Debit,
onClick = { paymentType = PaymentType.Debit }
onClick = { if (sdkState == SdkState.IDLE) paymentType = PaymentType.Debit },
enabled = sdkState == SdkState.IDLE
)
Text("Débito")
}
}
Spacer(modifier = Modifier.height(12.dp))
Button(
onClick = {
val amountValue = amount.toDoubleOrNull()?.times(100)?.toLong()
val installmentCount = installments.toIntOrNull() ?: 1
if (amountValue == null || amountValue <= 0) {
handler.post {
Toast.makeText(context, "Insira um valor válido", Toast.LENGTH_SHORT).show()
}
return@Button
}
if (!isServiceConnected) {
handler.post {
Toast.makeText(context, "Serviço não conectado", Toast.LENGTH_SHORT).show()
}
return@Button
}
if (!isPaying) {
isPaying = true
showLoading = true
viewModel.pay(
amount = amountValue,
installments = if (paymentType == PaymentType.Credit) installmentCount else null,
paymentType = paymentType,
allowContactless = true,
resolve = { response ->
isPaying = false
showLoading = false
handler.post {
Toast.makeText(context, "Pagamento realizado com sucesso!", Toast.LENGTH_LONG).show()
}
},
reject = { errorCode, errorMessage ->
isPaying = false
showLoading = false
handler.post {
Toast.makeText(context, "Erro no pagamento: $errorMessage", Toast.LENGTH_LONG).show()
}
}
)
},
enabled = sdkState == SdkState.IDLE,
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(
containerColor = if (sdkState == SdkState.PAYING)
MaterialTheme.colorScheme.secondary
else
MaterialTheme.colorScheme.primary
)
) {
Text(
when (sdkState) {
SdkState.PAYING -> "Processando Pagamento..."
SdkState.IDLE -> "Realizar Pagamento"
else -> "Aguarde..."
}
)
}
}
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
Button(
onClick = {
viewModel.abortOperation(
resolve = {
handler.post {
Toast.makeText(context, "Operação cancelada", Toast.LENGTH_SHORT).show()
}
},
enabled = !isPaying,
modifier = Modifier.fillMaxWidth()
reject = { errorCode, errorMessage ->
handler.post {
Toast.makeText(context, "Erro ao cancelar: $errorMessage", Toast.LENGTH_SHORT).show()
}
}
)
},
enabled = sdkState in listOf(SdkState.PAYING, SdkState.PROCESSING, SdkState.CANCELING),
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error)
) {
Text(if (isPaying) "Processando Pagamento..." else "Realizar Pagamento")
Text("Cancelar Operação")
}
if (message.isNotEmpty() && !showLoading) {
Text(
text = "Mensagem do PIN pad: $message",
style = MaterialTheme.typography.bodyMedium
Button(
onClick = {
viewModel.deactivate(
resolve = { success ->
handler.post {
Toast.makeText(
context,
if (success) "SDK desativado com sucesso" else "Falha ao desativar SDK",
Toast.LENGTH_SHORT
).show()
}
},
reject = { errorCode, errorMessage ->
handler.post {
Toast.makeText(context, "Erro na desativação: $errorMessage", Toast.LENGTH_SHORT).show()
}
}
)
},
enabled = sdkState == SdkState.IDLE,
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondary)
) {
Text("Desativar SDK")
}
}
if (pinLength > 0) {
Text(
text = "Comprimento do PIN: $pinLength",
style = MaterialTheme.typography.bodyMedium
)
}
}
}
@Composable
fun LoadingScreen(message: String, onDismiss: () -> Unit) {
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background.copy(alpha = 0.8f)),
contentAlignment = Alignment.Center
if (sdkState in listOf(SdkState.INITIALIZING, SdkState.PAYING, SdkState.PROCESSING, SdkState.CANCELING)) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.background(MaterialTheme.colorScheme.surface)
.padding(16.dp)
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
CircularProgressIndicator()
Spacer(modifier = Modifier.height(8.dp))
Text(
text = message.ifEmpty { "Processando..." },
style = MaterialTheme.typography.bodyLarge
text = when (sdkState) {
SdkState.INITIALIZING -> "Inicializando SDK..."
SdkState.PAYING -> "Processando pagamento..."
SdkState.PROCESSING -> "Processando operação..."
SdkState.CANCELING -> "Cancelando operação..."
else -> "Aguarde..."
},
style = MaterialTheme.typography.bodyMedium
)
if (message.isNotEmpty()) {
Text(
text = message,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
}
}
private fun getSdkStateText(state: SdkState): String {
return when (state) {
SdkState.NOT_INITIALIZED -> "Não Inicializado"
SdkState.INITIALIZING -> "Inicializando"
SdkState.IDLE -> "Pronto"
SdkState.PROCESSING -> "Processando"
SdkState.PAYING -> "Pagando"
SdkState.CANCELING -> "Cancelando"
}
}

View File

@ -10,12 +10,12 @@ import android.os.IBinder
import android.util.Log
import br.com.aditum.IAditumSdkService
import br.com.aditum.data.v2.model.MerchantData
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.withContext
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
class PaymentApplication : Application() {
companion object {
@ -55,81 +55,14 @@ class PaymentApplication : Application() {
}
}
private var mServiceConnectionListener: OnServiceConnectionListener? = null
var serviceConnectionListener: OnServiceConnectionListener?
get() = mServiceConnectionListener
set(listener) {
mServiceConnectionListener = listener
}
interface OnServiceConnectionListener {
fun onServiceConnection(serviceConnected: Boolean)
}
override fun onCreate() {
super.onCreate()
Log.d(TAG, "onCreate")
startAditumSdkService()
}
private var serviceConnectionListener: OnServiceConnectionListener? = null
override fun onTerminate() {
super.onTerminate()
Log.d(TAG, "onTerminate")
}
fun startAditumSdkService() {
Log.d(TAG, "startAditumSdkService")
val intent = Intent(ACTION_COMMUNICATION_SERVICE).apply {
setPackage(PACKAGE_NAME)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Log.d(TAG, "Android Oreo or higher: ${Build.VERSION.SDK_INT}")
startForegroundService(intent)
} else {
Log.d(TAG, "Android Nougat or lower: ${Build.VERSION.SDK_INT}")
startService(intent)
}
bindService(intent, mServiceConnection, BIND_AUTO_CREATE or CONTEXT_IGNORE_SECURITY)
Log.d(TAG, "startAditumSdkService - bindService")
}
suspend fun ensureServiceConnected(): Boolean = withContext(Dispatchers.IO) {
if (isServiceConnected && mAditumSdkService?.asBinder()?.isBinderAlive == true) {
Log.d(TAG, "Service already connected")
return@withContext true
}
Log.d(TAG, "Service not connected, attempting to reconnect...")
val intent = Intent(ACTION_COMMUNICATION_SERVICE).apply {
setPackage(PACKAGE_NAME)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent)
} else {
startService(intent)
}
val bound = bindService(
intent,
mServiceConnection,
BIND_AUTO_CREATE or CONTEXT_IGNORE_SECURITY
)
if (bound) {
repeat(30) {
delay(100)
if (isServiceConnected && mAditumSdkService?.asBinder()?.isBinderAlive == true) {
Log.d(TAG, "Service reconnected successfully")
return@withContext true
}
}
}
Log.e(TAG, "Failed to reconnect service")
return@withContext false
fun setServiceConnectionListener(listener: OnServiceConnectionListener) {
this.serviceConnectionListener = listener
}
private fun setServiceConnected(isConnected: Boolean) {
@ -137,7 +70,53 @@ class PaymentApplication : Application() {
synchronized(this) {
mIsServiceConnected = isConnected
_isServiceConnectedFlow.value = isConnected
mServiceConnectionListener?.onServiceConnection(mIsServiceConnected)
serviceConnectionListener?.onServiceConnection(isConnected)
}
}
override fun onCreate() {
super.onCreate()
Log.d(TAG, "onCreate")
}
override fun onTerminate() {
super.onTerminate()
Log.d(TAG, "onTerminate")
}
fun startAditumSdkService(context: Context): Boolean {
Log.d(TAG, "startAditumSdkService")
val intent = Intent(ACTION_COMMUNICATION_SERVICE).apply {
setPackage(PACKAGE_NAME)
}
return try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
context.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE)
} catch (e: Exception) {
Log.e(TAG, "Failed to start or bind service: ${e.message}")
false
}
}
fun retryServiceConnection(context: Context, maxAttempts: Int = 3, delayMs: Long = 1000) {
var attempts = 0
while (attempts < maxAttempts && !isServiceConnected) {
if (startAditumSdkService(context)) {
return
}
Thread.sleep(delayMs)
attempts++
}
if (!isServiceConnected) {
Log.e(TAG, "Failed to connect to service after $maxAttempts attempts")
}
}
fun ensureServiceConnected(): Boolean {
return isServiceConnected
}
}