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:
android.enableJetifier=true
android.useAndroidX=true
kotlin.code.style=official
Add Gradle dependencies
Project-level build.gradle
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.
...
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:
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
.
<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:
- Login with Phone No
- Verify OTP
- 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:
private lateinit var psViewModel: AuthVm
In onCreate()
method, Give the definition of psViewModel
psViewModel = ViewModelProvider(this).get(AuthVm::class.java)
Create LiveData Observer for Auth Login Process:
/* 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:
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:
fun onClickSubmit() {
/*
phoneNumber = "CountryCode" + "Number"
eg. +15123412345, +919876543210
*/
val authLogin = PsAuthLoginReq(phoneNumber)
psViewModel.authLoginReq(authLogin)
}
Sample AuthEnterPhoneNumber Source Code:
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
private lateinit var psViewModel: AuthVm
In onCreate() method, you can give the definition of psViewModel
psViewModel = ViewModelProvider(this).get(AuthVm::class.java)
Create LiveData Observer for Auth Login Process
/* 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
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
fun onClickSubmit() {
val authOtpVerification = AuthLoginOtpVerificationReq(
isMobileInterface = true,
phoneNumber = phoneNumber,
deviceNumber = XpssInsta.deviceId,
otp = strOtp,
)
psViewModel.authOtpVerification(authOtpVerification)
}
Sample AuthOtpVerificationFrag
Source Code
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:
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
private lateinit var psViewModel: MReportVm
In onCreate() method, you can give the definition of psViewModel
psViewModel = ViewModelProvider(this).get(MReportVm::class.java)
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:
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
private lateinit var mTerminalVM: MTerminalVm
In onCreate()
method, you can give the definition of mTerminalVm
mTerminalVM = ViewModelProvider(this).get(MTerminalVm::class.java)
Create a LiveData Observer for the terminal transaction process.
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
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.
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.
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
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.
override fun onCardPresent() { }
d. onCardRemoved()
Simple event to warn higher layers that a card has been removed from the field.
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.
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.
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.
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.
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.
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.