Implementing the use of the SDK

1

Now that the dependency has been set up, you need to carry out the initial configuration. These settings are necessary to start the payment SDK. Let's define these settings in the Application class.

Create a class that extends Application and define the payment settings.

In your project's initialization class (Application), you should “extend” TapOnPhoneApplication

Call the isApplicationInitAllowed() method to check that the application can be initialized.

if (!TapOnPhoneInitializer.isApplicationInitAllowed(this)) return

TapOnPhoneInitializer.initializeTerminal(this)

The static initializeTerminal() method of the TapOnPhoneInitializer class must be executed. It initializes the terminal, an essential step for using the SDK.

To define the initial settings, run the setTerminalConfig(config: TerminalConfigEntity) method. To do this, create a TerminalConfigEntity object. The values entered below are for the sandbox scenario.

data class TerminalConfigEntity(
    val companyDocument: String?, // O número do CNPJ (Cadastro Nacional de Pessoa Jurídica) da empresa. - Contém o número do CNPJ da empresa.
    val companyName: String?,     // O nome da empresa.
    val merchantId: UUID?,        // O identificador único do comerciante, fornecido pela equipe FirstTech.
    val terminalNumber: String?,  // O número único atribuído ao terminal, fornecido pela equipe FirstTech.
    val clientId: String?,        // O ID do Cliente usado para autenticação, fornecido pela equipe FirstTech. 
    val clientSecret: String?,    // O Client Secret usado para autenticação, fornecido pela equipe FirstTech.
    val sdkScope: String?,        // O escopo necessário para autenticação do SDK, fornecido pela equipe FirstTech e enviado pelo consumidor deste SDK.
    val sdkClientId: String?,     // O ID do Cliente para autenticação do SDK, fornecido pela equipe FirstTech e enviado pelo consumidor deste SDK.
    val sdkClientSecret: String?, // O Client Secret para autenticação do SDK, fornecido pela equipe FirstTech e enviado pelo consumidor deste SDK.
    val packageName: String?,     // O nome do pacote da aplicação do cliente, enviado pelo consumidor deste SDK.O Nome do Pacote é usado para referência de onde a aplicação do cliente está sendo executada, enviado pelo consumidor deste SDK.
    val appVersion: String?,      // A versão atual da aplicação do cliente, enviado pelo consumidor deste SDK. - A Versão do Aplicativo é uma informação sobre a versão do aplicativo do cliente atual, enviado pelo consumidor deste SDK.
    val sdkOrganization: String?  // O nome da organização, enviado pelo consumidor deste SDK.
)

At the end of this process, the file structure should look like the one below.

Important: The values shown refer to the sandbox environment.

2

The project is already set up. Next, let's modify MainActivity. Below is an example layout to illustrate the use of our SDK. Feel free to adapt it to suit your layout. Our activity follows this model.

3

All of the SDK's manipulation operations are carried out via the ViewModel, which receives the TapOnPhoneApplication as a parameter when it is created.

At the start of the Activity, we have the following declarations:

private val progressDialog by lazy { ProgressDialog(this) } //<- This Android user interface component is used to show a progress dialog box.
private val binding: ActivityMainBinding by lazy { ActivityMainBinding.inflate(layoutInflater) } //<- Define the binding variable as ActivityMainBinding. //<- This is a class automatically generated by Android's View Binding feature.
private val mppProviderViewModel by lazy { MPPPProviderViewModel(application as Application) } //<- ViewModel to manage the data and logic of the user interface.


  override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)
        setupView()
        setupObserver()
        askPermission()
        getVersionCode()
    }

4

Our MainActivity looks like this.

OBS: We use the askPermission method to request permission to collect the user's location, information that will be used for better error analysis later.

private fun askPermission() {
        val permissionsToRequest = mutableListOf<String>()

        if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
            != PackageManager.PERMISSION_GRANTED
        ) {
            permissionsToRequest.add(Manifest.permission.ACCESS_FINE_LOCATION)
        }


        if (permissionsToRequest.isNotEmpty()) {
            Log.d("Signal", "Solicitando permissões.")

            ActivityCompat.requestPermissions(
                this,
                permissionsToRequest.toTypedArray(),
                ALL_PERMISSIONS_REQUEST_CODE
            )
        } else {
            Log.d("Signal", "Todas as permissões já concedidas.")
        }
    }
5

Start a payment

Execute the startPaymentFlow() function provided by MPPProviderViewModel. To do this, you need to pass the TransactionInfoEntity class as a parameter, which receives the following values in its constructor.

installments:An integer containing the number of parcels

paymentType: One of the options PaymentType , which can be PaymentType.DEBIT or PaymentType.CREDIT

amount: The total amount of the transaction, expressed Long .

Value

Long

R$ 23,34

2334

R$ 15,00

1500

R$ 0,15

15

R$0,01

1

 private fun startPayment() {
   mppProviderViewModel.startPaymentFlow( // <- Este é o método que inicia o fluxo de pagamento
        TransactionInfoEntity(
            operationType, //<- Define o tipo da operação
            ((binding.etValor.text.toString().toDigits() ?: 0.0) * 100).toLong(), // <- Valor da transação, multiplicado por 100 (para representar centavos)
            binding.etInstallments.text.toString().toInt() //<- Número de parcelas  //<- Número de parcelas (quando o pagamento é a crédito)
            )
        )
    }
    

Manual stoppage of the payment process

If the payment flow is terminated, either by user action or by an internal flow of the application itself, you must use the method provided to terminate the session (transaction). The clearCurrentTerminalSession() method of the SessionHelper object must be called. This action is essential because otherwise the previous session would remain active. Consequently, when trying to move on to the transaction screen again, there would be an error in creating a new session, since there would already be one in progress.

This method can be called at any point in your application. In the examples below, we demonstrate how to implement it in Kotlin and React Native.

To use it in Kotlin, just call the following method:

SessionHelper.clearCurrentTerminalSession()

In the React Native application, we will use the “Back” button to trigger the session termination method provided by the SDK. The implementation detailed below is specific to the React Native environment.

import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.Promise
import com.firsttech.taponphone.sdk.v2.utils.DeviceInformationUtils
import com.firsttech.taponphone.sdk.v2.utils.SessionHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import android.util.Log

class DeviceInfoModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
    override fun getName(): String = "DeviceInfoModule"

    @ReactMethod
    fun getDeviceId(promise: Promise) {
        try {
            val id = DeviceInformationUtils.getDeviceIdentifier(reactApplicationContext)
            promise.resolve(id)
        } catch (e: Exception) {
            promise.reject("ERROR", e.message)
        }
    }

    @ReactMethod
    fun getDeviceInfo(promise: Promise) {
        CoroutineScope(Dispatchers.Default).launch {
            try {
                val info = DeviceInformationUtils.getDeviceInfo(reactApplicationContext)
                promise.resolve(info)
            } catch (e: Exception) {
                promise.reject("DEVICE_INFO_ERROR", e.message)
            }
        }
    }
    //


    fun addListener(eventName: String?) {
        // Requerido pelo NativeEventEmitter
    }

    fun removeListeners(count: Int) {
        // Requerido pelo NativeEventEmitter
    }

    @ReactMethod
    fun clearTerminalSession() {
        try {
            SessionHelper.clearCurrentTerminalSession()
        } catch (e: Exception) {
            Log.e("TerminalModule", "Erro ao limpar sessão do terminal", e)
        }
    }
}

Now we'll define how to use our new method, located in the DeviceInfoModule file, via the JavaScript wrapper that we've used before for other functions. Feel free to create a new file dedicated to this functionality or integrate it into the existing file in our sample application.


import { NativeModules } from 'react-native';

const { DeviceInfoModule } = NativeModules;

export function getDeviceInfo(): Promise<string> {
  return DeviceInfoModule.getDeviceInfo();
}

export function clearTerminal(): void {
  DeviceInfoModule.clearTerminalSession();
}

In our index file (or index.tsx), which defines our application's payment processing screen, we will import the clearTerminal method - previously defined in our native module - to be used when the “Back” button is pressed.

import { clearTerminal } from '../../native-modules/DeviceInfoModule

We'll also make the following imports

BackHandler: Manages Android's physical "Back" button. useFocusEffect: Executes effects (such as adding listeners from the BackHandler) when the screen gains focus and clears them when it loses focus. useCallback: Memoizes the function passed to useFocusEffect, optimizing it and ensuring that it is stable, preventing unnecessary recreations and executions of the focus effect.

And in our component that works as a "Back" button, we'll call the clearTerminal() method, implemented earlier. This, in turn, will use the session clearing method implemented in the SDK.

 <BackButtonWrapper
            onPress={() => {
              clearTerminal(); 
              goBack();        
            }}
        >
        <BackButtonArrowLeftVector source={arrowLeftVector} />
      </BackButtonWrapper>

6

Reacting to messages

TheMPPProviderViewModel provides oLiveData onMessage, which will receive an “extended” object from the MPPViewData class.

It is important to observe onMessage within the correct lifecycle, ensuring that there are no memory leaks or unexpected reactions.

In this project, we used the concept of Sealed Classes for a more efficient abstraction. Below, we list all the objects that can be returned in onMessage and their respective meanings.

Here, we get the reference to LiveData onMessage and start observing it:

private fun setupObserver() {
   mppProviderViewModel.onMessage.observe(this, ::onMessage)
}

Note: In this example, we haven't listed all the MPPViewData classes, but we recommend implementing all the classes.

In our repository, our sample will contain a list of all the classes, which we can have in our when

Note: Some MPPViewData classes contain objects that help you understand the message that should be displayed in a specific step.


private fun onMessage(viewData: MPPViewData?) {
        viewData?.let {
            when (viewData) {
                MPPViewData.TerminalInitializingViewData -> {
                    showLoadingDialog("Inicializando terminal")
                    binding.tvMessage.text = "Inicializando terminal"
                }

                MPPViewData.TerminalInitializingErrorViewData -> {
                    showFeedbackDialog("Erro ao inicializar terminal")
                    progressDialog.dismiss()
                }

                MPPViewData.TerminalInitializingSuccessViewData -> {
                    progressDialog.dismiss()
                    binding.tvMessage.text = "Inicialização concluída"
                }
                
                is MPPViewData.TerminalPaymentSuccessViewData -> {
                    enableEditFields(true)
                    binding.tvMessage.text =
                        if (viewData.result.isProcessingResultSuccess()) "Pagamento realizado" else "Pagamento recusado"
                    val metaData = viewData.result.getMetaDataTranslated()

                    binding.tvLog.text = "" +
                            "Resultado: ${viewData.result.uiMessage.messageId.sampleUiMessage}" +
                            createReceipt(metaData)
                }
                
                //Mapear todos os cenários de MPPViewData

            }
        }
    }

In this example TerminalPaymentSuccessViewData is when the transaction was successfully completed.

OBS: To access the messageJson, retrieve the result you received in viewData and extract the messageJson as follows

val metaData = viewData.result.getMetaDataTranslated()
val messageJson = metaData?.receipt?.merchant?.messageJson

7

Printing the receipt on the screen

Here is the implementation of the createReceipt function method, which returns a String. This string is set up to be a receipt and can be customized in any way you need.


package com.firsttech.taponphone.app.utils

import java.text.NumberFormat
import java.util.Locale
import com.firsttech.taponphone.sdk.v2.models.TransactionCompleted

fun createReceipt(metaDataTranslated: TransactionCompleted.Metadata?):String {
    val messageJson = metaDataTranslated?.receipt?.merchant?.messageJson
    val parcelas = messageJson?.transaction?.installments?.total ?: 0
    val valor = messageJson?.transaction?.amount

    return "\n${messageJson?.acquirer}" +
            "\n" +
            "\nCNPJ: ${messageJson?.businessDocument}" +
            "\nTID: ${messageJson?.transactionId}" +
            "\n" +
            "\n${messageJson?.card?.number}" +
            "\n${messageJson?.card?.brand}" +
            "\nAID: ${messageJson?.card?.aid}" +
            "\n${messageJson?.transaction?.datetime}" +
            "\nCV: ${messageJson?.transaction?.stan}" +
            "\nValor: ${messageJson?.transaction?.currency} ${valor?.let { it1 -> setCurrencyFormat(it1) }}" +
            "\nForma de Pagamento:  ${messageJson?.transaction?.paymentMethod}" +
            (if(messageJson?.transaction?.paymentMethod.equals("Crédito")){
                "\n${if(parcelas==1)"À vista" else "Valor de cada parcela: ${messageJson?.transaction?.currency} ${setCurrencyFormat(messageJson?.transaction?.installments?.value.toString())}" }" +
                        "\nQuantidade de parcela:  ${messageJson?.transaction?.installments?.total}"
            }else{
                "\n${setTextInstallments(parcelas)}"
            }) +
            "\nTerm: ${messageJson?.terminalCode}" +
            "\n" +
            "\nDADOS ORIGINAIS DA VENDA" +
            "\nValor: ${messageJson?.transaction?.currency} ${messageJson?.transaction?.amount?.let { it1 ->
                setCurrencyFormat(
                    it1
                )
            }}" +
            "\nTerm: ${messageJson?.terminalCode}" +
            "\n${messageJson?.transaction?.messageAuthorizedBy}"
}

private fun setCurrencyFormat(amount:String): String {
    val formatCurrency = NumberFormat.getCurrencyInstance(Locale("pt", "BR"))
    val amountToDouble = formatCurrency.format(amount.toDouble())

    val amountConverted = amountToDouble.replace("R$", "").trim()
    return amountConverted
}
private fun setTextInstallments(installements:Int): String {
    return if (installements==1) "À vista" else "Parcelado em $installements vezes"
}

Here's an example of the JSON with the messageJson field that we used to create our receipt


"receipt":{
      "merchant":{
         "message":"Via Loja\n\nCNPJ: 00.595.154/0001-73\nTID: 010000865033\n\n**** **** **** 2029\nMASTERCARD\nAID: A0000000041010\n17/06/25 11:32\nCV: 241393\nValor: R$ 50,00\nForma de pagamento: Crédito\nValor de cada parcela: 5\nQuantidade de parcelas: 10\nTerm: 00000026\n\nDADOS ORIGINAIS DA VENDA\nValor: R$ 50,00\nTerm: 00000026\nTransação autorizada pelo emissor\n",
         "messageJson":{
            "acquirer":"Via Loja",
            "merchant":"First Customer",
            "businessDocument":"00.595.154/0001-73",
            "transactionId":"010000865033",
            "card":{
               "number":"**** **** **** 2029",
               "brand":"MASTERCARD",
               "aid":"A0000000041010"
            },
            "transaction":{
               "amount":50,
               "currency":"BRL",
               "datetime":"17/06/25 11:32:25",
               "stan":"241393",
               "installments":{
                  "value":5,
                  "total":10
               },
               "paymentMethod":"Crédito",
               "messageAuthorizedBy":"Transação autorizada pelo emissor"
            },
            "terminalCode":"00000026"
         }
      },

Description of each messageJson field

  • acquirer → Name of the acquirer or payment facilitator (e.g., "Via Loja")

  • merchant → Name of the merchant or store (e.g., "First Customer")

  • businessDocument → Tax ID of the merchant (CNPJ in Brazil)

  • transactionId → Transaction identifier (TID - Terminal ID)

  • card → Information about the card used in the transaction:

    • number → Masked card number (last visible digits)

    • brand → Card brand (e.g., MASTERCARD, VISA)

    • aid → Application Identifier (EMV card type identifier)

  • transaction → Details of the transaction:

    • amount → Total transaction amount (e.g., 50)

    • currency → Currency used in the transaction (e.g., BRL)

    • datetime → Date and time of the transaction

    • stan → CV or STAN (System Trace Audit Number) — unique transaction identifier

    • installments → Installment details:

      • value → Value of each installment

      • total → Total number of installments

    • paymentMethod → Payment method (e.g., Credit)

    • messageAuthorizedBy → Authorization message of the transaction (e.g., "Transaction authorized by issuer")

  • terminalCode → Code of the terminal (e.g., POS device) that processed the transaction

8

Minimum value lock

Starting with version 1.0.29, the SDK has the function of preventing a transaction from being initiated if the transaction value exceeds the minimum limit previously registered in the back-end.

Below is the implementation of how this should be done when this condition is met, giving us the option to send a message as feedback to the user

Following the same example above, “6-Responding to messages,” from the moment version 1.0.29 is consumed, Kotlin itself will inform that a new viewData (“TerminalPaymentValueBelowMinimumLimitViewData”) has not been placed as a condition within the when.

We will insert this new condition and within it we will place the message that will be displayed to the user. Within viewData, we have the field (minValue), which will contain the registered minimum value

Kotlin

Here is an example of implementation

     is MPPViewData.TerminalPaymentValueBelowMinimunLimitViewData -> {
                    progressDialog.dismiss()
                    showFeedbackDialog("O valor do pagamento está abaixo do limite mínimo R$${viewData.minValue}")
                    binding.tvMessage.text = "O valor do pagamento está abaixo do limite mínimo."
                }
            }

React

Follow the implementation example

    is MPPViewData.TerminalPaymentValueBelowMinimunLimitViewData ->{
                      sendErrorMessage("O valor do pagamento está abaixo do limite mínimo R$${viewData.minValue}")
                }
9

Responding to invalid transactions

Starting with version 1.0.30, the SDK prevents a transaction from being initiated if certain parameters are incorrect (amount and installments).

When this occurs, specific ViewData (PaymentInformationInvalidViewData) will be sent.

The app needs to listen for these messages and, when this ViewData appears, notify the user with a message.

Kotlin

Follow the implementation example

  is  MPPViewData.PaymentInformationInvalidViewData -> {
                    showFeedbackDialog("Pagamento com parametros incorretos.")
                    binding.tvMessage.text = "Revise as condições de pagamento."
                }

React Native

Follow the implementation example

           is MPPViewData.PaymentInformationInvalidViewData -> {
                    sendErrorMessage("Condicoes de pagamento incorretos.")
                }

Last updated