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

Let’s do it!

To keep this article short, I have already created the project credit_card_info_finder. I will explain the project structure

The project is segmented into four folders using MVVM architecture.

Enough of the introduction , let start coding!

copy these code in your app/build.gradle file

//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'

sync the file

Let set up our Retrofit base class. Copy and paste these code below

RetrofitClient.class

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()
}


}

ApiInterface.class

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


interface ApiInterface {


@GET("{bin}")
fun getCard(@Path("bin") id: String?): Call<CardDetails>
}

CardRepository.class

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")
}
}
})
}
}

We have set up connection to the server 👯‍.

Let create our view model
MainViewModel.class

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)
}


}

We are almost done 🤗

It time to bind everything together. Let create our MainActivity.class ,It extend an abtract class(BaseActivity)

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)
}

}





}

Boom we are done🤸🏻🤸🏽‍♂️🤸‍♀️

I tried my best to make this lesson clear and short as much as possible. Please, bear with me for any error you might encounter. I am just trying to share my knowledge.
Stay tuned for the IOS version.

If you have any question or a correction, I will be glad to know.

Project Repo — https://github.com/muryno/credit_card_info_finder/

You can mail me personally —murainoy@yahoo.com

Github | Linkedin | Twitter

Thanks for reading.

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