Pace Software Android SDK integration

This document details information regarding the Pace Software SDK Integration in Android Application. Detailed technical information in this document is used to implement, design, and develop seamless payment applications.

1. Add Pacesoft SDK to your project

Android X

Make sure you've added the following to your gradle.properties file:

Copy
Copied
android.enableJetifier=true
android.useAndroidX=true
kotlin.code.style=official

Add Gradle dependencies

Project-level build.gradle

Copy
Copied
buildscript {
   ext.kotlin_version = '1.5.31'

   repositories {
       google()
       jcenter()
       mavenCentral()
   }

   dependencies {
       classpath 'com.android.tools.build:gradle:7.2.1'
       ...
   }
}

allprojects {
   repositories {
       google()
       jcenter()
       mavenCentral()

       maven { url "https://jitpack.io" }
       maven { url "https://sdk.pacesoft.com/android/sdk/amidis/release" }
       ...
   }
}


task clean(type: Delete) {
   delete rootProject.buildDir
}

App-level build.gradle

Enable multi-dex, Java 8, and other option as following:

Attention

minSdk version should be at least 29 or above.

Copy
Copied
...

android {
   ...
   defaultConfig {
       minSdk 29

       ...
   }

   ...

   compileOptions {
       sourceCompatibility = JavaVersion.VERSION_1_8
       targetCompatibility = JavaVersion.VERSION_1_8
   }

   kotlinOptions {
       jvmTarget = JavaVersion.VERSION_1_8
   }

   packagingOptions {
       resources {
           excludes += ['META-INF/atomicfu.kotlin_module', 'META-INF/viewpump_release.kotlin_module']
       }
       pickFirst 'google/protobuf/*'
   }
}

dependencies {
   ...

   // ViewModel & LiveData
   implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
   implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.0'

//ProcessLifecycleOwner
implementation 'android.arch.lifecycle:extensions:1.1.1'

   ...
}

Then add the dependencies. Replace the [latest-version] with 0.0.1. implementation 'com.pacesoft.android:pacesoft-amadis:[latest-version]'

2. Pacesoft SDK Initialization

Setup main application subclass

The best place to initialize the Pacesoft SDK is in the onCreate method of your Application subclass.

If you don't already have this class, create it:

Copy
Copied
class MainApp : Application(), LifecycleObserver {

   ...

   private val mPaceSoftCallback = object : PaceSoftCallback {
       override fun onForceUserLogout() {
           /*
           Handling Pacesoft SDK logout

           This will get trigger in couple of cases
           1. If pDefend detected some Threat in Android devices.
           2. If ClientApp logs out from the Pacesoft SDK manually.

           */
       }
   }


   override fun onCreate() {
       super.onCreate()
       ...

       ProcessLifecycleOwner.get().lifecycle.addObserver(this)

       PaceSoftSdk.init(
           application = this,
           appId = BuildConfig.paceSoftAppId,
           callback = mPaceSoftCallback,
       )
   }

   ...

}

You have to integrate PaceSoftCallback object in case, PaceSoft SDK forces logout the session.

Register this class in your AndroidManifest.xml:

Attention

Be careful not to confuse the onCreate method in the main activity with the onCreate method from the Activity.

Copy
Copied
<application
   android:name=".MainApp"
   ...
   >
Attention

In some cases, it is possible to set up the Pacesoft SDK from inside an activity onCreate method, but it is not recommended.

3. LiveData with Resource Sealed Class

For accessing the Pacesoft SDK module you have to authorize the user and get the session from the server. Almost 99% of the response and session related parts will be handled by SDK, so you have to sit back and consume them.

All the Request-Response related stuff is managed using MVVM Design pattern. Developer has to integrate LiveData dependency in the build.gradle file. There could be many ways of handling responses coming from SDK in android but in Pacesoft SDK we have chosen to handle it with the help of Kotlin Sealed Class. This sealed class approach fits perfectly in this case: If the user is expecting data in the UI, then we need to send the errors all the way to our UI to notify the user & to make sure the user won’t just see a blank screen or experience an unexpected UI. We have just created a generic sealed class named Resource in a separate file in Pacesoft data package x.code.util.repo. We will notify the user UI on these three events: Loading, Success, Error.

We created these as child classes of Resources to represent different states of UI.:

  • Upon loading we’ll return a Resource.Loading object .
  • Upon a success , we’ll get data so we’ll wrap it with Resource.Success .
  • Upon an error , we’ll wrap the error message with Resource.Error .

4. Authentication Screen

For accessing the Pacesoft SDK module you have to authorize the user and get the session from the server. Virtually all of the response and session related events are handled by SDK.

The following are the steps for authorizing a user:

  1. Login with Phone No
  2. Verify OTP
  3. Get Authorize to access SDK

Login Flow

In this flow, PaceSoft SDK required a user phone number as input parameter to authorize the user for further transaction and report.

Pacesoft SDK will take the PhoneNumber, Validate the PhoneNumber, and then create a session against the shared PhoneNumber.

Client App can by-pass this first page (Insert PhoneNumber), if the phone number already exists at their end, and can navigate directly to the VerifyOTP page.

Enter Phone Number Page

Declare the Pacesoft AuthVm in instance level of your EnterPhoneNumber Fragment/Activity:

Copy
Copied
private lateinit var psViewModel: AuthVm

In onCreate() method, Give the definition of psViewModel

Copy
Copied
psViewModel = ViewModelProvider(this).get(AuthVm::class.java)

Create LiveData Observer for Auth Login Process:

Copy
Copied
/* Pacesoft View Model Observer for Auth Login */
private val psvmObsAuthLogin = Observer<Resource<AuthLoginRsp?>> {

   it?.let { resource ->
       when (resource.status) {
           Status.LOADING -> {
               showProgress(true)
           }
           Status.SUCCESS -> {
               if (!isAdded) return@Observer
               showProgress(false)
               popApiRsp(resource.data)
           }
           Status.ERROR -> {
               showProgress(false)
               popApiRsp(null)
           }
       }
   }
}

Important is to attach and detach LiveData Observer on bases of the view lifecycle:

Copy
Copied
fun attachObservers() {
   psViewModel.ldAuthLogin.observe(this, psvmObsAuthLogin)
}

fun detachObservers() {
   psViewModel.ldAuthLogin.removeObserver(psvmObsAuthLogin)
}

User can request the Login to PaceSoft SDK with phone number, onClick of submit button:

Copy
Copied
fun onClickSubmit() {
   /*
   phoneNumber = "CountryCode" + "Number"
   eg. +15123412345, +919876543210
   */
   val authLogin = PsAuthLoginReq(phoneNumber)
   psViewModel.authLoginReq(authLogin)
}

Sample AuthEnterPhoneNumber Source Code:

Copy
Copied
class AuthEnterPhoneNumberFrag : Fragment() {

   private lateinit var psViewModel: AuthVm

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)

       ...
       psViewModel = ViewModelProvider(this).get(AuthVm::class.java)
       attachObservers()
   }

   fun attachObservers() {
       psViewModel.ldAuthLogin.observe(this, psvmObsAuthLogin)
   }

   fun detachObservers() {
       psViewModel.ldAuthLogin.removeObserver(psvmObsAuthLogin)
   }

   fun onClickSubmit() {
       /*
       phoneNumber = "CountryCode" + "Number"
       eg. +15123412345, +919876543210
       */
       val authLogin = PsAuthLoginReq(phoneNumber)
       psViewModel.authLoginReq(authLogin)
   }

   /* Pacesoft View Model Observer for Auth Login */
   private val psvmObsAuthLogin = Observer<Resource<AuthLoginRsp?>> {

       it?.let { resource ->
           when (resource.status) {
               Status.LOADING -> {
                   showProgress(true)
               }
               Status.SUCCESS -> {
                   if (!isAdded) return@Observer
                   showProgress(false)
                   popApiRsp(resource.data)
               }
               Status.ERROR -> {
                   showProgress(false)
                   popApiRsp(null)
               }
           }
       }
   }

   private fun showProgress(flag: Boolean) {
      mAuthActivity.showProgress(flag)
      etPhoneNumber.isEnabled = !flag
      etPhoneNumber.hideKeyboard()
   }

   private fun popApiRsp(item: AuthLoginRsp?) {
       if (item != null) {
           snack("OTP send successfully")
           //Navigate to Verify OTP Screen
       } else {
           snack(R.string.something_went_wrong_msg_1)
       }
   }

   override fun onDestroy() {
       super.onDestroy()
       detachObservers()
   }
}

Once this step returns a success, redirect the user to the VerifyOTP page.

Verify OTP Page

You have to declare the Pacesoft AuthVm in instance level of your Verify OTP Fragment/Activity

Copy
Copied
private lateinit var psViewModel: AuthVm

In onCreate() method, you can give the definition of psViewModel

Copy
Copied
psViewModel = ViewModelProvider(this).get(AuthVm::class.java)

Create LiveData Observer for Auth Login Process

Copy
Copied
/* Pacesoft View Model Observer for OTP Verification */
private val psvmObsAuthOtpVerification =
   Observer<Resource<AuthLoginOtpVerificationRsp?>> {

       it?.let { resource ->
           when (resource.status) {
               Status.LOADING -> {
                   showProgress(true)
               }
               Status.SUCCESS -> {
                   if (!isAdded) return@Observer
                   showProgress(false)
                   val data = resource.data
                   popApiRsp(data)
               }
               Status.ERROR -> {
                   showProgress(false)

                   x.code.util.view.XDialog.bottomSheet1(
                       activity = mActivity,
                       bottomSheetDialog = BottomSheetDialog(requireContext()),
                       msg = resource.msg ?: "Login OTP failed, please try after sometime.",
                       title = getString(x.code.R.string.attention),
                       positiveText = getString(R.string.retry),
                       positiveClickMethod = {
                           clearOtpField()
                       }
                   )
               }
           }
       }
   }

It is important to attach and detach LiveData Observer on bases of the view lifecycle

Copy
Copied
fun attachObservers() {
   psViewModel.ldLoginOtpVerification.observe(this,
                                       psvmObsAuthOtpVerification)
}

fun detachObservers() {
   psViewModel.ldLoginOtpVerification.removeObserver(
                                       psvmObsAuthOtpVerification)
}

User can request the Login to PaceSoft SDK with phone number, onClick of submit button

Copy
Copied
fun onClickSubmit() {
   val authOtpVerification = AuthLoginOtpVerificationReq(
       isMobileInterface = true,
       phoneNumber = phoneNumber,
       deviceNumber = XpssInsta.deviceId,
       otp = strOtp,
   )

   psViewModel.authOtpVerification(authOtpVerification)
}

Sample AuthOtpVerificationFrag Source Code

Copy
Copied
class AuthOtpVerificationFrag : Fragment() {

   private lateinit var psViewModel: AuthVm

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)

       ...
       psViewModel = ViewModelProvider(this).get(AuthVm::class.java)
       attachObservers()
   }

   fun attachObservers() {
       psViewModel.ldLoginOtpVerification.observe(this, psvmObsAuthOtpVerification)
   }

   fun detachObservers() {
       psViewModel.ldLoginOtpVerification.removeObserver(psvmObsAuthOtpVerification)
   }

   fun onClickSubmit() {
       val authOtpVerification = AuthLoginOtpVerificationReq(
           isMobileInterface = true,
           phoneNumber = phoneNumber,
           deviceNumber = XpssInsta.deviceId,
           otp = strOtp,
       )

       psViewModel.authOtpVerification(authOtpVerification)
   }

   /* Pacesoft View Model Observer for OTP Verification */
   private val psvmObsAuthOtpVerification =
       Observer<Resource<AuthLoginOtpVerificationRsp?>> {

           it?.let { resource ->
               when (resource.status) {
                   Status.LOADING -> {
                       showProgress(true)
                   }
                   Status.SUCCESS -> {
                       if (!isAdded) return@Observer
                       showProgress(false)
                       val data = resource.data
                       popApiRsp(data)
                   }
                   Status.ERROR -> {
                       showProgress(false)

                       x.code.util.view.XDialog.bottomSheet1(
                           activity = mActivity,
                           bottomSheetDialog = BottomSheetDialog(requireContext()),
                           msg = resource.msg ?: "Login OTP failed, please try after sometime.",
                           title = getString(x.code.R.string.attention),
                           positiveText = getString(R.string.retry),
                           positiveClickMethod = {
                               clearOtpField()
                               etOtp1.requestFocus()
                           }
                       )
                   }
               }
           }
       }

   private fun popApiRsp(item: AuthLoginRsp?) {
       if (item != null) {
           snack("OTP verified")
           //Ready to use PaceSoft SDK
       }
   }

   private fun showProgress(show: Boolean) {
       //Show Progress UI according your standard UI design
   }

   override fun onDestroy() {
       super.onDestroy()
       detachObservers()
   }
}

5. Merchant Transaction Report Page

Users can fetch historical transactions reports using Offset (Lazy Loading). Client developer has the responsibility to manage the offset variable properly at their end. Mismanage of this variable can lead to duplicates of records or other erratic behavior.

The following is the code for accessing the Transaction Report of a user:

Copy
Copied
private fun reqMerchantTransactionReport() {
   val req = MerchantTxnReq(
      skip = offset,
      take = 10, //can be a static value, to take max item at once
      sortColumn = "openAt",
      sortOrder = -1,
      searchText = "",
      term = ""
   )

   psViewModel.getMerchantTrasactionList(req)
}

You have to declare the Pacesoft AuthVm in instance level of your Verify OTP Fragment/Activity

Copy
Copied
private lateinit var psViewModel: MReportVm

In onCreate() method, you can give the definition of psViewModel

javajava
Copy
Copied
psViewModel = ViewModelProvider(this).get(MReportVm::class.java)
Copy
Copied
private val apiObs_4_Reports =
   Observer<Resource<Pair<MerchantTxnReq, MerchantTxnSummaryRsp?>>> {
       it?.let { resource ->
           when (resource.status) {
               Status.LOADING -> {
                   showProgress(true)
               }
               Status.SUCCESS -> {
                   if (!isAdded) return@Observer
                   showProgress(false)
                   val data = resource.data
                   popApiRsp(data)
               }
               Status.ERROR -> {
                   showProgress(false)
                   popApiRsp(null)
                   showNoData(true)
               }
           }
       }
   }

API Response consist of following elements:

Copy
Copied
data class MerchantTransactionReportRsp(
   val response: String?,
   val customer: String?,
   val status: String?,
   val typeOfTransaction: String?,
   val amountTotal: Double?,
   val dateTime: String?,
   val cardLast4Digit: String?,
   val cardBrand: String?,
   val transactionId: Long?,
   val clientName: String?,
)

6. Merchant Transaction Page

For making user transaction on your user device, you have to follow below steps

You have to declare the Pacesoft MTerminalVm in instance level of Fragment/Activity

Copy
Copied
private lateinit var mTerminalVM: MTerminalVm

In onCreate() method, you can give the definition of mTerminalVm

Copy
Copied
mTerminalVM = ViewModelProvider(this).get(MTerminalVm::class.java)

Create a LiveData Observer for the terminal transaction process.

Copy
Copied
private val apiObsSale = Observer<Resource<SaleTxnV2Rsp?>> {
   	it?.let { resource ->
        	when (resource.status) {
             	Status.LOADING -> {
                    showProgress(true)
            	}
            	Status.SUCCESS -> {
                    if (!isAdded) return@Observer
                    showProgress(false)
                    val data = resource.data
                    //handleApiRsp
            	}
            	Status.ERROR -> {
                    showProgress(false)
                    popApiRsp(null, resource.msg)
                 	}
        	}
    	}}

It is important to attach and detach LiveData Observer on bases of the view lifecycle

Copy
Copied
fun attachObservers() {
   mTerminalVm.ldSale.observe(this, apiObsSale)
 }

fun detachObservers() {
   mTerminalVm.ldSale.removeObserver(apiObsSale)
}

I. Initialize Payment SDK

To initialize agnos payment sdk there are three parameters required. These three parameters are context, activity and AgnosPayCallback reference. This is the first step before starting any transaction. Make sure this step takes some extra milliseconds, Don’t start a transaction immediately after initializing payment sdk — wait for the respective call back method. Use the following code to initialize the payment SDK.

Copy
Copied
XpssInsta.agnosPaySDK.initializeAgnos(context,activity,
                                           agnosPayCallback)
Parameters Description
context Current context
activity Current Activity reference
agnosPayCallback AgnosPayCallback reference

AgnoPayCallback

It is an interface that is used to inform UI about the transaction process. The following methods are implemented to use AgnosPayCallback:

a. onInitializeCompleted (purchase: Transaction)

This call back informs the UI about the successful initialization of payment sdk and this will provide parameters of the Transaction thread that is necessary to start the transaction thread.

Copy
Copied
override fun onInitializeCompleted(purchase: Transaction)
{
    this.purchase = purchase
}

b. onInitializeFailed (message:String)

This call back informs the UI about the initialization failure. This call back function has a parameter message that contains the reason for failure. Once this (AgnosPaySDK) initialization fails, it is recommended to terminate sdk and reinitialize it before starting the transaction process. Use the following code to implement this call back function

Copy
Copied
override fun onInitializeFailed(message: String) {
	 terminateAgnos()
 }
private fun terminateAgnos() {
	 XpssInsta.agnosPaySDK.terminateAgnos()
}

c. onCardPresent()

Simple event to warn higher layers that a card has been presented into the field.

Copy
Copied
override fun onCardPresent() {  }

d. onCardRemoved()

Simple event to warn higher layers that a card has been removed from the field.

Copy
Copied
override fun onCardRemoved() {  }

e. transactionIsReady()

This event informs the UI that the transaction request is ready. This is the step where you can show a message on UI to remove the card or you can stop card reading, countdown and other UI handling. Once your UI work is finished then you need to provide a terminal view-model reference by calling processSaleRequest(mTerminalVm) and this method allows agnos pay sdk to send current transaction requests to the host server.

Copy
Copied
override fun transactionIsReady() {
      //"Please remove card, Transaction request send .."
     XpssInsta.agnosPaySDK.processSaleRequest(mTerminalVm)
}

f. transactionDeclined(msg: String)

This call back function warns the UI that due to card decline by the terminal the transaction has been declined. The message parameter contains the reason for decline.

Copy
Copied
override fun transactionDeclined(msg: String){ }

g. txnCancelled(error: String)

This call back function warns the UI that a transaction is canceled due to some error occurring at the time of pre-processing transaction data, card reader timeout and data processing.

Copy
Copied
override fun txnCancelled(error: String) { }

II. Check Valid Key

Before starting a transaction you need to check if the valid HSM key is present in the sdk. If it is available then you can go for the transaction otherwise you need to logout and reinitialize agnos pay sdk. Following code snippet to check valid amadis key is present or not.

Copy
Copied
if (!XpssInsta.keysPref.isValidAmadisKey()) {
	x.code.util.view.XDialog.alertDialogA1(
    	 activity = requireActivity(),
    	 title = R.string.attention,
    	 msg = "Session Expired. Proceed to logout",
    	 positiveText = R.string.logout,
    	 positiveClickMethod = { Logout() }
		)
 }

III. Start Transaction

To start a transaction using agnos pay sdk, a separate thread started to run the Transaction thread provided in onInitializeCompleted callback. But first I need to pass the amount to the transaction thread and sdk. Following code snippet to start a transaction with agnos SDK.

if (purchase != null) { currentThread = Thread { purchase!!.setAmount(amountS.toLong()) XpssInsta.agnosPaySDK.startTransaction( XInsta.getTerminalCartAmountSum()) XpssInsta.agnosPaySDK.resetTxn() purchase!!.run() } currentThread?.start() }else { snack("Initializing Agnos ...") initializeAgnos() XCoroutines.main { delay(200) // Now startTransaction } }

IV. Cancel Transaction

Cancel transaction calling is recommended after the transaction is interrupted by backpress, transaction declined or transaction failed.

Copy
Copied
if (purchase != null) {
XpssInsta.agnosPaySDK.cancelTxn()
purchase.cancel()
}
Attention

This guide details the steps needed to initialize the SDK and start a transaction. See the sample application for advanced features and functionality supplied by the SDK.

Copyright © Pace Software 2021–2023. All rights reserved.