Compare commits

...

10 Commits

14 changed files with 1283 additions and 355 deletions

View File

@ -0,0 +1,61 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
</profile>
</component>

View File

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

Binary file not shown.

BIN
app/mypos.jks Normal file

Binary file not shown.

View File

@ -2,6 +2,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<queries>
<package android:name="br.com.aditum.smartpostef" />
</queries>
<application <application
android:name=".services.PaymentApplication" android:name=".services.PaymentApplication"
android:allowBackup="true" android:allowBackup="true"

View File

@ -1,25 +1,29 @@
package com.example.mypos package com.example.mypos
import android.content.Context
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.ViewModelProvider
import com.example.mypos.models.PaymentViewModel
import com.example.mypos.screen.PaymentScreen
import com.example.mypos.services.PaymentApplication
import com.example.mypos.ui.theme.POSTheme import com.example.mypos.ui.theme.POSTheme
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() val context = this
var paymentApplication = application as PaymentApplication
var paymentViewModel = ViewModelProvider(
this,
PaymentViewModel.provideFactory(paymentApplication, context)
)[PaymentViewModel::class.java]
setContent { setContent {
POSTheme { POSTheme {
PaymentScreen(paymentViewModel)
} }
} }
} }

View File

@ -0,0 +1,12 @@
package com.example.mypos.data
import br.com.aditum.data.v2.enums.AbecsCommands
import br.com.aditum.data.v2.enums.TransactionStatus
interface PaymentRegister {
fun notification(
message: String?,
transactionStatus: TransactionStatus?,
command: AbecsCommands?
)
}

View File

@ -1,31 +1,681 @@
package com.example.mypos.models package com.example.mypos.models
import android.content.Context import android.content.Context
import android.util.Log
import androidx.datastore.preferences.core.edit
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import br.com.aditum.data.v2.IPaymentCallback
import br.com.aditum.data.v2.enums.AbecsCommands
import br.com.aditum.data.v2.enums.InstallmentType
import br.com.aditum.data.v2.enums.PayOperationType
import br.com.aditum.data.v2.enums.PaymentType
import br.com.aditum.data.v2.enums.TransactionStatus
import br.com.aditum.data.v2.model.Charge
import br.com.aditum.data.v2.model.MerchantData
import br.com.aditum.data.v2.model.PinpadMessages
import br.com.aditum.data.v2.model.callbacks.GetClearDataFinishedCallback
import br.com.aditum.data.v2.model.callbacks.GetClearDataRequest
import br.com.aditum.data.v2.model.callbacks.GetMenuSelectionFinishedCallback
import br.com.aditum.data.v2.model.callbacks.GetMenuSelectionRequest
import br.com.aditum.data.v2.model.cancelation.CancelationRequest
import br.com.aditum.data.v2.model.cancelation.CancelationResponse
import br.com.aditum.data.v2.model.cancelation.CancelationResponseCallback
import br.com.aditum.data.v2.model.deactivation.DeactivationResponseCallback
import br.com.aditum.data.v2.model.init.InitRequest
import br.com.aditum.data.v2.model.init.InitResponse
import br.com.aditum.data.v2.model.init.InitResponseCallback
import br.com.aditum.data.v2.model.payment.PaymentRequest
import br.com.aditum.data.v2.model.payment.PaymentResponse
import br.com.aditum.data.v2.model.payment.PaymentResponseCallback
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.preferences.PrefKeys
import com.example.mypos.preferences.dataStore
import com.example.mypos.services.PaymentApplication import com.example.mypos.services.PaymentApplication
import com.google.gson.Gson
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch 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( class PaymentViewModel(
private val application: PaymentApplication, private val application: PaymentApplication,
private val context: Context private val context: Context
) : ViewModel() { ) : ViewModel() {
private val paymentApplication = application as PaymentApplication 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) private val _isServiceConnected = MutableStateFlow(false)
val isServiceConnected: StateFlow<Boolean> = _isServiceConnected.asStateFlow() 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 { init {
viewModelScope.launch { viewModelScope.launch {
paymentApplication.isServiceConnectedFlow.collect { isConnected -> paymentApplication.isServiceConnectedFlow.collect { isConnected ->
_isServiceConnected.value = isConnected _isServiceConnected.value = isConnected
if (isConnected) {
try {
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() { private val notificationCallback = object : IPaymentCallback.Stub() {
paymentApplication.startAditumSdkService() override fun notification(
message: String,
transactionStatus: TransactionStatus,
command: AbecsCommands
) {
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) {
viewModelScope.launch {
_pinLengthFlow.emit(length)
_messageFlow.emit("PIN entrada: $length dígitos")
}
}
override fun startGetClearData(
clearDataRequest: GetClearDataRequest?,
finished: GetClearDataFinishedCallback?
) {
if (clearDataRequest != null) {
Log.d(TAG, "clearDataRequest: ${clearDataRequest.title} ${clearDataRequest.alphaNumeric} ${clearDataRequest.timeout}")
}
}
override fun startGetMenuSelection(
clearDataRequest: GetMenuSelectionRequest?,
finished: GetMenuSelectionFinishedCallback?
) {
// Implementação customizada se necessário
}
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)
}
)
}
}
fun initAditumSdk(
applicationName: String,
applicationVersion: String,
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 {
_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
}
val initRequest = InitRequest().apply {
this.pinpadMessages = pinpadMessages
this.activationCode = if (BuildConfig.DEBUG) BuildConfig.ACTIVATION_CODE else activationCode
this.applicationName = applicationName
this.applicationVersion = applicationVersion
this.useOnlySdk = true
this.applicationToken = BuildConfig.APPLICATION_TOKEN
}
val callback = object : InitResponseCallback.Stub() {
override fun onResponse(initResponse: InitResponse?) {
viewModelScope.launch {
val json = gson.toJson(initResponse)
Log.d(TAG, json)
if (initResponse != null && initResponse.initialized) {
_sdkState.value = SdkState.IDLE
if (activationCode != null) {
saveActivationCode(activationCode)
clearNewActivationCode()
}
resolve(true)
} else {
_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)
?: 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 pay(
amount: Long,
installments: Int? = null,
paymentType: PaymentType,
allowContactless: Boolean = true,
amountSeasoning: ((Long) -> Long)? = null,
resolve: (PaymentResponse) -> Unit = {},
reject: (String, String?) -> Unit = { _, _ -> }
) {
viewModelScope.launch {
try {
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
}
val paymentRequest = PaymentRequest().apply {
currency = 986
operationType = PayOperationType.Authorization
this.paymentType = paymentType
this.amount = amount
this.allowContactless = allowContactless
manualEntry = false
installmentType = if (paymentType == PaymentType.Debit) InstallmentType.Undefined else InstallmentType.Merchant
this.installmentNumber = when {
installments == null -> if (paymentType == PaymentType.Credit) 1 else 0
installments > 1 -> installments
paymentType == PaymentType.Debit -> 0
else -> 1
}
}
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, "Payment response: $json")
resolve(paymentResponse)
} else {
val errorMsg = "Resposta de pagamento nula"
setError(AditumError.PAYMENT_RESPONSE_NULL, errorMsg)
reject(AditumError.PAYMENT_RESPONSE_NULL, errorMsg)
}
}
}
}
paymentApplication.communicationService?.pay(paymentRequest, callback)
?: 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}")
}
}
}
fun confirm(nsu: String, resolve: (Boolean) -> Unit = {}, reject: (String, String?) -> Unit = { _, _ -> }) {
viewModelScope.launch {
try {
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)
?: 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 cancel(nsu: String, isReversal: Boolean, resolve: (Boolean) -> Unit = {}, reject: (String, String?) -> Unit = { _, _ -> }) {
viewModelScope.launch {
try {
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 {
val errorMsg = "Resposta de cancelamento nula"
setError(AditumError.CANCEL_RESPONSE_NULL, errorMsg)
reject(AditumError.CANCEL_RESPONSE_NULL, errorMsg)
}
}
}
}
paymentApplication.communicationService?.cancel(cancelationRequest, callback)
?: 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 abortOperation(resolve: (Boolean) -> Unit = {}, reject: (String, String?) -> Unit = { _, _ -> }) {
viewModelScope.launch {
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 = { _, _ -> }) {
viewModelScope.launch {
try {
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.d(TAG, "Pending transactions: $json")
resolve(results)
}
}
}
paymentApplication.communicationService?.pendingTransactions(callback)
?: 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 (!isServiceConnected.value) {
reject(AditumError.SERVICE_NOT_AVAILABLE, AditumError.SERVICE_NOT_AVAILABLE_MESSAGE)
return@launch
}
val callback = object : DeactivationResponseCallback.Stub() {
override fun onResponse(status: Boolean) {
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,
context: Context
): ViewModelProvider.Factory = object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(PaymentViewModel::class.java)) {
return PaymentViewModel(paymentApplication, context) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
} }
} }

View File

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

View File

@ -0,0 +1,436 @@
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 com.example.mypos.models.SdkState
@Composable
fun PaymentScreen(viewModel: PaymentViewModel) {
val context = LocalContext.current
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()
val error by viewModel.errorFlow.collectAsState()
val scrollState = rememberScrollState()
val handler = remember { Handler(Looper.getMainLooper()) }
LaunchedEffect(error) {
error?.let { (errorCode, errorMessage) ->
handler.post {
Toast.makeText(context, "Erro: $errorMessage", Toast.LENGTH_LONG).show()
}
}
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
.verticalScroll(scrollState),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = "Sistema de Pagamento POS",
style = MaterialTheme.typography.headlineMedium
)
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 (activationCode.isBlank()) {
handler.post {
Toast.makeText(context, "Código de ativação não pode estar vazio", Toast.LENGTH_SHORT).show()
}
return@Button
}
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 = BuildConfig.VERSION_NAME,
activationCode = codeToUse,
resolve = { success ->
handler.post {
Toast.makeText(
context,
if (success) "SDK inicializado com sucesso!" else "Falha na inicialização do SDK",
Toast.LENGTH_LONG
).show()
}
},
reject = { errorCode, errorMessage ->
handler.post {
Toast.makeText(context, "Erro: $errorMessage", Toast.LENGTH_LONG).show()
}
}
)
},
enabled = sdkState !in listOf(SdkState.INITIALIZING, SdkState.PROCESSING, SdkState.PAYING, SdkState.CANCELING),
modifier = Modifier.fillMaxWidth()
) {
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(),
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 && sdkState == SdkState.IDLE
)
Spacer(modifier = Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
Row(verticalAlignment = Alignment.CenterVertically) {
RadioButton(
selected = 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 = { 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
}
viewModel.pay(
amount = amountValue,
installments = if (paymentType == PaymentType.Credit) installmentCount else null,
paymentType = paymentType,
allowContactless = true,
resolve = { response ->
handler.post {
Toast.makeText(context, "Pagamento realizado com sucesso!", Toast.LENGTH_LONG).show()
}
},
reject = { errorCode, errorMessage ->
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()
}
},
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("Cancelar Operação")
}
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 (sdkState in listOf(SdkState.INITIALIZING, SdkState.PAYING, SdkState.PROCESSING, SdkState.CANCELING)) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
CircularProgressIndicator()
Spacer(modifier = Modifier.height(8.dp))
Text(
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

@ -1,267 +0,0 @@
package com.example.mypos.services
import android.util.Log
import br.com.aditum.data.v2.enums.InstallmentType
import br.com.aditum.data.v2.enums.PayOperationType
import br.com.aditum.data.v2.enums.PaymentType
import br.com.aditum.data.v2.model.Charge
import br.com.aditum.data.v2.model.MerchantData
import br.com.aditum.data.v2.model.PinpadMessages
import br.com.aditum.data.v2.model.cancelation.CancelationRequest
import br.com.aditum.data.v2.model.cancelation.CancelationResponse
import br.com.aditum.data.v2.model.cancelation.CancelationResponseCallback
import br.com.aditum.data.v2.model.deactivation.DeactivationResponseCallback
import br.com.aditum.data.v2.model.init.InitRequest
import br.com.aditum.data.v2.model.init.InitResponse
import br.com.aditum.data.v2.model.init.InitResponseCallback
import br.com.aditum.data.v2.model.payment.PaymentRequest
import br.com.aditum.data.v2.model.payment.PaymentResponse
import br.com.aditum.data.v2.model.payment.PaymentResponseCallback
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.google.gson.Gson
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class AditumSdkService(private val paymentApplication: PaymentApplication) {
companion object {
private const val TAG = "AditumSdkModule"
}
private val gson = Gson()
private val coroutineScope = CoroutineScope(Dispatchers.IO)
fun getName(): String = "AditumSdkModule"
fun initAditumSdk(
applicationName: String,
applicationVersion: String,
activationCode: String?,
resolve: (Boolean) -> Unit = {},
reject: (String, String?) -> Unit = { _, _ -> }
) {
coroutineScope.launch {
try {
if (!paymentApplication.ensureServiceConnected()) {
reject(AditumError.SERVICE_NOT_AVAILABLE, AditumError.SERVICE_NOT_AVAILABLE_MESSAGE)
return@launch
}
val pinpadMessages = PinpadMessages().apply {
mainMessage = applicationName
}
val initRequest = InitRequest().apply {
this.pinpadMessages = pinpadMessages
this.activationCode = if (BuildConfig.DEBUG) BuildConfig.ACTIVATION_CODE else activationCode
this.applicationName = applicationName
this.applicationVersion = applicationVersion
this.applicationToken = BuildConfig.APPLICATION_TOKEN
}
val callback = object : InitResponseCallback.Stub() {
override fun onResponse(initResponse: InitResponse?) {
if (initResponse != null) {
resolve(initResponse.initialized)
} else {
Log.e(TAG, "onResponse - initResponse is null")
reject(AditumError.INIT_RESPONSE_NULL, AditumError.INIT_RESPONSE_NULL_MESSAGE)
}
}
}
paymentApplication.communicationService?.init(initRequest, callback)
?: reject(AditumError.SERVICE_NULL, AditumError.SERVICE_NULL_MESSAGE)
} catch (e: Exception) {
reject(AditumError.INIT_ERROR, "${AditumError.INIT_ERROR_MESSAGE}: ${e.message}")
}
}
}
fun getMerchantData(resolve: (MerchantData) -> Unit = {}, reject: (String, String?) -> Unit = { _, _ -> }) {
coroutineScope.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 = 0,
paymentType: PaymentType,
allowContactless: Boolean = true,
amountSeasoning: ((Long) -> Long)? = null,
resolve: (PaymentResponse) -> Unit = {},
reject: (String, String?) -> Unit = { _, _ -> }
) {
coroutineScope.launch {
try {
if (!paymentApplication.ensureServiceConnected()) {
reject(AditumError.SERVICE_NOT_AVAILABLE, AditumError.SERVICE_NOT_AVAILABLE_MESSAGE)
return@launch
}
val adjustedAmount = amountSeasoning?.invoke(amount) ?: amount
if (adjustedAmount <= 0) {
reject(AditumError.INVALID_AMOUNT, AditumError.INVALID_AMOUNT_MESSAGE)
return@launch
}
val paymentRequest = PaymentRequest().apply {
currency = 986
operationType = PayOperationType.Authorization
this.paymentType = paymentType
this.amount = amount
this.allowContactless = allowContactless
manualEntry = false
installmentType = if (paymentType == PaymentType.Debit) InstallmentType.None else InstallmentType.Merchant
installmentNumber = installments
}
val callback = object : PaymentResponseCallback.Stub() {
override fun onResponse(paymentResponse: PaymentResponse?) {
if (paymentResponse != null) {
val json = gson.toJson(paymentResponse)
Log.e(TAG, "onResponse - $json")
resolve(paymentResponse)
} else {
Log.e(TAG, "onResponse - paymentResponse is null")
reject(AditumError.PAYMENT_RESPONSE_NULL, AditumError.PAYMENT_RESPONSE_NULL_MESSAGE)
}
}
}
paymentApplication.communicationService?.pay(paymentRequest, callback)
?: reject(AditumError.SERVICE_NULL, AditumError.SERVICE_NULL_MESSAGE)
} catch (e: Exception) {
reject(AditumError.PAYMENT_ERROR, "${AditumError.PAYMENT_ERROR_MESSAGE}: ${e.message}")
}
}
}
fun confirm(nsu: String, resolve: (Boolean) -> Unit = {}, reject: (String, String?) -> Unit = { _, _ -> }) {
coroutineScope.launch {
try {
if (!paymentApplication.ensureServiceConnected()) {
reject(AditumError.SERVICE_NOT_AVAILABLE, AditumError.SERVICE_NOT_AVAILABLE_MESSAGE)
return@launch
}
val callback = object : ConfirmTransactionCallback.Stub() {
override fun onResponse(confirmed: Boolean) {
resolve(confirmed)
}
}
paymentApplication.communicationService?.confirmTransaction(nsu, callback)
?: reject(AditumError.SERVICE_NULL, AditumError.SERVICE_NULL_MESSAGE)
} catch (e: Exception) {
reject(AditumError.CONFIRM_ERROR, "${AditumError.CONFIRM_ERROR_MESSAGE}: ${e.message}")
}
}
}
fun cancel(nsu: String, isReversal: Boolean, resolve: (Boolean) -> Unit = {}, reject: (String, String?) -> Unit = { _, _ -> }) {
coroutineScope.launch {
try {
if (!paymentApplication.ensureServiceConnected()) {
reject(AditumError.SERVICE_NOT_AVAILABLE, AditumError.SERVICE_NOT_AVAILABLE_MESSAGE)
return@launch
}
val cancelationRequest = CancelationRequest(nsu, isReversal)
val callback = object : CancelationResponseCallback.Stub() {
override fun onResponse(cancelationResponse: CancelationResponse?) {
if (cancelationResponse != null) {
resolve(cancelationResponse.canceled)
} else {
Log.e(TAG, "onResponse - cancelationResponse is null")
reject(AditumError.CANCEL_RESPONSE_NULL, AditumError.CANCEL_RESPONSE_NULL_MESSAGE)
}
}
}
paymentApplication.communicationService?.cancel(cancelationRequest, callback)
?: reject(AditumError.SERVICE_NULL, AditumError.SERVICE_NULL_MESSAGE)
} catch (e: Exception) {
reject(AditumError.CANCEL_ERROR, "${AditumError.CANCEL_ERROR_MESSAGE}: ${e.message}")
}
}
}
fun aboutOperation(resolve: (Boolean) -> Unit = {}, reject: (String, String?) -> Unit = { _, _ -> }) {
coroutineScope.launch {
if (!paymentApplication.ensureServiceConnected()) {
reject(AditumError.SERVICE_NOT_AVAILABLE, AditumError.SERVICE_NOT_AVAILABLE_MESSAGE)
return@launch
}
paymentApplication.communicationService?.abortOperation()
}
}
fun pendingTransactions(resolve: (List<Charge?>?) -> Unit = {}, reject: (String, String?) -> Unit = { _, _ ->}) {
coroutineScope.launch {
try {
if (!paymentApplication.ensureServiceConnected()) {
reject(AditumError.SERVICE_NOT_AVAILABLE, AditumError.SERVICE_NOT_AVAILABLE_MESSAGE)
return@launch
}
val callback = object : PendingTransactionsCallback.Stub() {
override fun onResponse(results: List<Charge?>?) {
val json = gson.toJson(results)
Log.e(TAG, "onResponse - $json")
resolve(results)
}
}
paymentApplication.communicationService?.pendingTransactions(callback)
?: reject(AditumError.SERVICE_NULL, AditumError.SERVICE_NULL_MESSAGE)
} catch (e: Exception) {
reject(AditumError.CONFIRM_ERROR, "${AditumError.CONFIRM_ERROR_MESSAGE}: ${e.message}")
}
}
}
fun deactivate(resolve: (Boolean) -> Unit = {}, reject: (String, String?) -> Unit = { _, _ -> }) {
coroutineScope.launch {
try {
if (!paymentApplication.ensureServiceConnected()) {
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")
resolve(status)
}
}
paymentApplication.communicationService?.deactivate(callback)
?: reject(AditumError.SERVICE_NULL, AditumError.SERVICE_NULL_MESSAGE)
} catch (e: Exception) {
reject(AditumError.DEACTIVATE_ERROR, "${AditumError.DEACTIVATE_ERROR_MESSAGE}: ${e.message}")
}
}
}
}

View File

@ -10,20 +10,20 @@ import android.os.IBinder
import android.util.Log import android.util.Log
import br.com.aditum.IAditumSdkService import br.com.aditum.IAditumSdkService
import br.com.aditum.data.v2.model.MerchantData 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.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.withContext import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
class PaymentApplication : Application() { class PaymentApplication : Application() {
companion object { companion object {
private const val TAG = "PaymentApplication"
private const val PACKAGE_BASE_NAME = "br.com.aditum" private const val PACKAGE_BASE_NAME = "br.com.aditum"
const val PACKAGE_NAME = "$PACKAGE_BASE_NAME.smartpostef" const val PACKAGE_NAME = "$PACKAGE_BASE_NAME.smartpostef"
const val ACTION_COMMUNICATION_SERVICE = "$PACKAGE_BASE_NAME.AditumSdkService" const val ACTION_COMMUNICATION_SERVICE = "$PACKAGE_BASE_NAME.AditumSdkService"
} }
private val TAG = "PaymentApplication"
private val _isServiceConnectedFlow = MutableStateFlow(false) private val _isServiceConnectedFlow = MutableStateFlow(false)
val isServiceConnectedFlow: StateFlow<Boolean> = _isServiceConnectedFlow.asStateFlow() val isServiceConnectedFlow: StateFlow<Boolean> = _isServiceConnectedFlow.asStateFlow()
@ -55,81 +55,14 @@ class PaymentApplication : Application() {
} }
} }
private var mServiceConnectionListener: OnServiceConnectionListener? = null
var serviceConnectionListener: OnServiceConnectionListener?
get() = mServiceConnectionListener
set(listener) {
mServiceConnectionListener = listener
}
interface OnServiceConnectionListener { interface OnServiceConnectionListener {
fun onServiceConnection(serviceConnected: Boolean) fun onServiceConnection(serviceConnected: Boolean)
} }
override fun onCreate() { private var serviceConnectionListener: OnServiceConnectionListener? = null
super.onCreate()
Log.d(TAG, "onCreate")
startAditumSdkService()
}
override fun onTerminate() { fun setServiceConnectionListener(listener: OnServiceConnectionListener) {
super.onTerminate() this.serviceConnectionListener = listener
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
} }
private fun setServiceConnected(isConnected: Boolean) { private fun setServiceConnected(isConnected: Boolean) {
@ -137,7 +70,49 @@ class PaymentApplication : Application() {
synchronized(this) { synchronized(this) {
mIsServiceConnected = isConnected mIsServiceConnected = isConnected
_isServiceConnectedFlow.value = 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")
} }
} }
} }

View File

@ -0,0 +1,40 @@
package com.example.mypos.services
import android.graphics.Bitmap
import android.util.Log
import br.com.aditum.data.v2.enums.PrintStatus
import br.com.aditum.device.callbacks.IPrintStatusCallback
import com.google.gson.Gson
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
class PrintService(private val paymentApplication: PaymentApplication) {
companion object {
private const val TAG = "PrintService"
}
private val coroutineScope = CoroutineScope(Dispatchers.IO)
private val _printStatusFlow = MutableStateFlow(null)
val printStatusFlow: StateFlow<PrintStatus?> = _printStatusFlow.asStateFlow()
private val callback = object : IPrintStatusCallback.Stub() {
override fun finished(printStatus: PrintStatus?) {
coroutineScope.launch {
Log.d(TAG, "PrintStatus - $printStatus")
}
}
}
fun print(vararg bitmaps: Bitmap) {
bitmaps.forEach { bitmap ->
paymentApplication.communicationService?.deviceSdk?.printerSdk?.print(
bitmap,
callback
)
}
}
}