Optical Character Recognition (OCR) to read credit/debit card on Android using PayCards_Android and BINLIST API

What are we building?

In this tutorial, we are going to develop an Android app that makes use of PayCards_Android to read the card information and use BINLIST API to get credit card details and display the card information on the app.

Prerequisites

This lesson assumed you are familiar with android studio and understand the basics of the Kotlin programming language. Create a new project on android studio

  • View folder — It contains the activity folder for holding all your UI components and the base folder is an abstract class for activities that share the same properties.
  • ViewModel folder — It contains Mainviewmodel class -It is responsible for preparing and managing the data for an Activity and CardRepository class for making a network call to the server and communicating the data to ViewModel or local database.
//network
implementation 'com.squareup.retrofit2:retrofit:2.6.2'
implementation 'com.squareup.retrofit2:converter-gson:2.6.2'
implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2'


//card scanning

implementation 'cards.pay:paycardsrecognizer:1.1.0'

//view model
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
import ApiInterface
import com.google.gson.GsonBuilder
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit


class RetrofitClient {
companion object{

val BASE_URL: String = "https://lookup.binlist.net/"



private var mInstance: RetrofitClient? = null

@Synchronized
fun getInstance(): RetrofitClient? {
if (mInstance == null) {
mInstance =
RetrofitClient()
}
return mInstance
}
}
private var mRetrofit: Retrofit? = null

init {


val interceptor: HttpLoggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}

val client = OkHttpClient.Builder()
.readTimeout(3, TimeUnit.MINUTES)
.connectTimeout(3, TimeUnit.MINUTES)
.addInterceptor { chain ->
val newRequest = chain.request().newBuilder()
.addHeader("Content-Type", "application/json")
.build()
chain.proceed(newRequest)

}.addInterceptor(interceptor).build()

val gson = GsonBuilder()
.setLenient()
.create()


mRetrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
}
fun getApi(): ApiInterface? {
return mRetrofit?.create(ApiInterface::class.java)
}

fun reset() {
mInstance = null
getInstance()
}


}
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Path


interface ApiInterface {


@GET("{bin}")
fun getCard(@Path("bin") id: String?): Call<CardDetails>
}
import com.model.entity.CardDetails
import com.model.server.RetrofitClient
import com.view.view.CardView
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response

class CardRepository {



fun getCardDetails(card_num : String,callback : CardView){

RetrofitClient.getInstance()?.getApi()?.getCard(card_num)?.enqueue(object :
Callback<CardDetails> {
override fun onFailure(call: Call<CardDetails>, t: Throwable) {
callback.loadingFailed(t.toString())
}

override fun onResponse(call: Call<CardDetails>, response: Response<CardDetails>) {

if(response.isSuccessful && response.body()!= null){
when (response.code()){
200->{
response.body()?.let { callback.card(it)
}

callback.loadingSuccessful("valid card")

}

else ->{
callback.loadingFailed("Invalid request.. please try again")
}
}
}else{
callback.loadingFailed("Invalid request..Check your card details and try again")
}
}
})
}
}
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.model.entity.CardDetails
import com.viewmodel.repository.CardRepository
import com.view.view.CardView

class MainViewModel : ViewModel(), CardView {

var presenter: CardRepository = CardRepository()



//get success message from the server

var _sucessfful= MutableLiveData<String>()


//get error message from the server
var _error= MutableLiveData<String>()




//get card details from the server
var _card= MutableLiveData<CardDetails>()


//send card to server
fun postData(card_num : String){
presenter.getCardDetails(card_num,this)
}

override fun loadingFailed(msg: String) {
_error.postValue(msg)
}

override fun loadingSuccessful(msg: String) {
_sucessfful.postValue(msg)
}

override fun card(data: CardDetails?) {
_card.postValue(data)
}


}
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import androidx.core.content.ContextCompat
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import cards.pay.paycardsrecognizer.sdk.Card
import cards.pay.paycardsrecognizer.sdk.ScanCardIntent
import cards.pay.paycardsrecognizer.sdk.ScanCardIntent.CancelReason
import com.muryno.cardfinder.R
import com.view.base.BaseActivity
import com.viewmodel.MainViewModel
import com.utils.getCartLogo
import com.utils.getGreetingIcon
import com.utils.greetings
import com.utils.setCardNumber
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.card.*


class MainActivity : BaseActivity() {


private var viewModel : MainViewModel?=null

val REQUEST_CODE_SCAN_CARD = 1



override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

view()

}
//to keep the oncreate page clean
fun view(){
CardNumberListner()

//instantiate view model
viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
//kotlinx.android.synthetic bind ui component to res/activity_main.xml
btn.setOnClickListener {
scanCard()
}


viewModel?._error?.observe(this, Observer {
toastError(it)
progressDialog(false)

})

viewModel?._sucessfful?.observe(this, Observer {
toastSuccess(it)
progressDialog(false)
})


// to observe result from server and send through intent to display page
viewModel?._card?.observe(this , Observer {
if (it != null ){
val i = Intent(MainApplication.instance?.applicationContext,CardDetailActivity::class.java)
i.putExtra("data",it)
startActivity(i)

}

progressDialog(false)

})

}



//card number edit text listner
private fun CardNumberListner() {

edtCardNumber.addTextChangedListener(object : TextWatcher {
override fun onTextChanged(
s: CharSequence, start: Int, before: Int, count: Int) {}

override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
}

override fun afterTextChanged(s: Editable) { // Remove all spacing char


// to append logo to card ui
if(s.isNotEmpty()) {
providerLogo.visibility = View.VISIBLE
//handled in utils to set card logo
providerLogo.setImageResource(getCartLogo(s))
}else{
providerLogo.visibility = View.GONE
}




//logic to space card number
setCardNumber(s)
if(s.isNotEmpty()){
tv_card_number.text = s.toString()
}else{
tv_card_number.text = getString(R.string.card_number)
}



//get card details from server when edit text completed
postCardDetailsToServer(s)


}

})

}





///Here is the function that handle the ocr scan
private fun scanCard() {
val intent: Intent = ScanCardIntent.Builder(this).build()
startActivityForResult(intent, REQUEST_CODE_SCAN_CARD)
}

//get result of scanned card
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_CODE_SCAN_CARD) {
if (resultCode == Activity.RESULT_OK) {
val card: Card? = data?.getParcelableExtra(ScanCardIntent.RESULT_PAYCARDS_CARD)

setCard(card)
} else if (resultCode == Activity.RESULT_CANCELED) {
@CancelReason val reason: Int = data?.getIntExtra(
ScanCardIntent.RESULT_CANCEL_REASON,
ScanCardIntent.BACK_PRESSED
)
?: ScanCardIntent.BACK_PRESSED


} else if (resultCode == ScanCardIntent.RESULT_CODE_ERROR) {
// Log.i(cards.pay.sample.demo.CardDetailsActivity.TAG, "Scan failed")
}
}
}

//result from ocr and send to view model to post to the server
private fun setCard(card : Card?){
if(card!= null){
//show user progress bar before posting to the server
progressDialog(true)
viewModel?.postData(card.cardNumber.toString())
}
}
///instead of using the scan button, you can also type you card number in the text field
//post card details to server

private fun postCardDetailsToServer(s: Editable){

if(s.length == 19) {
val k: String = tv_card_number.text.toString().replace(" ", "")

//show user progress bar before posting to the server
progressDialog(true)
viewModel?.postData(k)
}

}





}

Senior mobile developer & Backend engineer ~ Flutter|Dart | JAVA | Kotlin| Spring framework | Golang