Skip to main content
NetPOS supports QR code-based payments through multiple providers, allowing merchants to generate QR codes for customers to scan or accept payments via displayed QR codes.

QR Payment Providers

NetPOS integrates with three major QR payment providers:

MasterPass QR

MasterCard’s QR payment solution with instant bitmap generation

NIBSS QR

Nigeria Inter-Bank Settlement System QR with auto-polling

BlueCode

European QR payment solution for contactless transactions

QR Payment Methods

NetPOS offers two QR payment approaches:

1. Merchant-Generated QR (Customer Scans)

Merchants generate a QR code that customers scan with their banking app:
1

Enter Amount

Merchant enters the transaction amount in the DisplayQrFragment
2

Generate QR Code

System generates a payment QR code via the contactless QR API
3

Customer Scans

Customer scans the displayed QR code with their mobile banking app
4

Payment Complete

Transaction is processed and merchant receives confirmation

2. Provider QR Selection (Merchant Selects Provider)

Merchants select from available QR providers (MasterPass, NIBSS, BlueCode):
1

Select Provider

Choose from MasterPass QR, NIBSS QR, or BlueCode in QRFragment
2

Enter Amount

Input transaction amount in the amount dialog
3

Display QR

Provider-specific QR code is generated and displayed in bottom sheet
4

Auto-Polling (NIBSS)

For NIBSS, system automatically polls for payment confirmation

Merchant-Generated QR Implementation

DisplayQrFragment

This fragment handles merchant-side QR code generation:
DisplayQrFragment.kt
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    initViews()
    val user = Singletons.gson.fromJson(Prefs.getString(PREF_USER, ""), User::class.java)
    netposID = user.netplus_id
    userTID = user.terminal_id
    name = user.business_name
    email = CONTACTLESS_TRANSACTION_DEFAULT_EMAIL
    
    loader = RandomNumUtil.alertDialog(requireContext())
    
    binding.process.setOnClickListener {
        processPayment()
    }
}

QR Code Generation

When merchant clicks process, the system generates a QR code:
private fun generateMerchantQr() {
    loader.show()
    val paymentWithQr = PayWithQrRequest(
        terminalId = userTID.toString(),
        netposId = Singletons.getConfigData()?.cardAcceptorIdCode ?: "",
        amount = amount.text.toString(),
        name = name.toString(),
        email = email.toString(),
        bank = RandomNumUtil.getBankName() ?: "",
        NPmerchantId = RandomNumUtil.getNetPlusPayMid(),
    )
    
    observeServerResponse(
        viewModel.paymentWithQr(paymentWithQr),
        loader,
        compositeDisposable,
        ioScheduler,
        mainThreadScheduler,
    ) {
        showFragment(
            targetFragment = DisplayQrResultFragment(),
            className = "Display QR Result"
        )
    }
}

PayWithQrRequest Model

The QR payment request includes:
PayWithQrRequest.kt
data class PayWithQrRequest(
    val terminalId: String,
    val netposId: String,
    val amount: String,
    val name: String,
    val email: String,
    val bank: String,
    val NPmerchantId: String
)

QR Code Display

After successful generation, the QR code is displayed:
DisplayQrResultFragment.kt
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    
    val displayQrResult = Prefs.getString(AppConstants.PAYMENT_WITH_QR_STRING, "")
    val imageBytes = Base64.decode(displayQrResult, Base64.DEFAULT)
    val decodedImage = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
    
    if (displayQrResult.isNotEmpty()) {
        Glide.with(requireContext()).load(decodedImage).into(binding.paymentQr)
    }
}

Provider QR Implementation

QRFragment Provider Selection

Merchants can select from available QR providers:
QRFragment.kt
private fun setService() {
    val listOfService = ArrayList<Service>()
        .apply {
            add(Service(0, "MasterPass QR", R.drawable.masterpass))
            add(Service(1, "NIBSS QR", R.drawable.ic_qr_code))
            add(Service(2, "BlueCode", R.drawable.ic_bluecode_logo))
        }
    adapter.submitList(listOfService)
}

Amount Dialog

Before generating QR, merchants enter the amount:
private fun showAmountDialog(serviceId: Int) {
    qrAmountDialog.show()
    qrAmoutDialogBinding.proceed.setOnClickListener {
        val amountDouble = qrAmoutDialogBinding.amount.text.toString().toDoubleOrNull()
        amountDouble?.let {
            qrAmountDialog.cancel()
            when (serviceId) {
                0 -> showMasterPassQRBottomSheetDialog(it)
                1 -> showNibssQRBottomSheet(it)
            }
        }
    }
}

MasterPass QR

QR Generation

MasterPass QR codes are generated via the QRViewModel:
QRViewModel.kt
fun getMasterPassQr(amount: Double) {
    val qrRequestBody = JsonObject()
    val user = Singletons.getCurrentlyLoggedInUser()!!
    qrRequestBody.apply {
        addProperty("amount", amount.toString())
        addProperty("order_id", UUID.randomUUID().toString())
        addProperty("merchant_id", user.netplus_id)
        addProperty("currency_code", "NGN")
        addProperty("country_code", "NG")
        addProperty("business_name", user.business_name)
        addProperty("merchant_city", "Lagos")
    }
    masterPassQRService.getStaticQr(qrRequestBody)
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .flatMap {
            val bmp = BitmapFactory.decodeStream(it.byteStream())
            if (bmp != null)
                Single.just(bmp)
            else
                throw NullPointerException("Bitmap is null")
        }
        .subscribe { t1, t2 ->
            t1?.let { bmp ->
                _masterPassQrBitmap.value = Event(bmp)
            }
            t2?.let { error ->
                qrErrorMessageMutableLiveData.value = Event("Error")
                message.value = Event("An error occurred while fetching QR")
            }
        }.disposeWith(disposable)
}

Display MasterPass QR

private fun showMasterPassQRBottomSheetDialog(amount: Double) {
    masterpassQrBottomSheetDialogBinding.progress.visibility = View.VISIBLE
    masterpassQrBottomSheetDialogBinding.providerQr.visibility = View.GONE
    masterpassQrBottomSheetDialogBinding.qr.setImageBitmap(null)
    masterPassQrBottomSheetDialog.show()
    viewModel.getMasterPassQr(amount)
}

Observe QR Bitmap

viewModel.masterPassQrBitmap.observe(viewLifecycleOwner) { event ->
    event.getContentIfNotHandled()?.let { bitmap ->
        masterpassQrBottomSheetDialogBinding.scanToPay.visibility = View.VISIBLE
        masterpassQrBottomSheetDialogBinding.progress.visibility = View.GONE
        masterpassQrBottomSheetDialogBinding.providerQr.visibility = View.VISIBLE
        masterpassQrBottomSheetDialogBinding.qr.setImageBitmap(bitmap)
    }
}

NIBSS QR

QR Generation with Auto-Polling

NIBSS QR includes automatic transaction status polling:
fun getNibssQR(amount: Double) {
    val start: Long = 111_111_111_111
    val end: Long = 999_999_999_999
    val range1 = (start..end).random()
    val range2 = (start..end).random()
    lastNibssOrderNumber.value = "${SimpleDateFormat("yMM", Locale.getDefault())
        .format(Date(System.currentTimeMillis()))}$range1$range2"
    
    val jsonObject = JsonObject()
    jsonObject.addProperty("amount", amount.toString())
    jsonObject.addProperty("order_no", lastNibssOrderNumber.value!!)
    
    nibssQRService.getQr(jsonObject)
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .flatMap {
            if (it.returnCode.isNullOrEmpty() || 
                it.returnCode != "Success" || 
                it.codeUrl.isNullOrEmpty())
                throw Exception("Could not fetch QR code")
            Single.just(encodeAsBitmap(it.codeUrl, 150, 150))
        }
        .subscribe { t1, t2 ->
            t1?.let {
                _nibssQRBitmap.value = Event(it)
                runHandler() // Start auto-polling
            }
            t2?.let {
                message.value = Event("Error")
            }
        }
        .disposeWith(disposable)
}

Transaction Status Polling

NIBSS QR automatically polls for payment confirmation:
private fun runHandler() {
    if (retryAttempts > 15) {
        stillHasRetryAttempts = false
        message.value = Event("Too many attempts without response")
        return
    }
    Handler(Looper.getMainLooper()).postDelayed({
        queryTransaction()
    }, 4000)
}

private fun queryTransaction() {
    retryAttempts += 1
    _reQuerying.value = Event(true)
    lastNibssOrderNumber.value?.let {
        val jsonObject = JsonObject()
        jsonObject.addProperty("order_no", it)
        nibssQRService.queryTransactionStatus(jsonObject)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .doFinally { _reQuerying.value = Event(false) }
            .subscribe { t1, t2 ->
                t1?.let { nibssQRResponse ->
                    when (nibssQRResponse.returnCode) {
                        "Paying" -> runHandler()
                        "Success" -> {
                            stillHasRetryAttempts = false
                            message.value = Event("Success, payment confirmed")
                        }
                        else -> {
                            message.value = Event("Failed, payment failed")
                            stillHasRetryAttempts = false
                        }
                    }
                }
                t2?.let {
                    retryAttempts -= 1
                    message.value = Event("Retrying")
                }
            }.disposeWith(disposable)
    }
}

Polling UI Feedback

viewModel.reQuerying.observe(viewLifecycleOwner) { event ->
    event.getContentIfNotHandled()?.let {
        if (it) {
            nibssQrBottomSheetDialogBinding.progress.visibility = View.VISIBLE
            nibssQrBottomSheetDialogBinding.scanToPay.text = 
                getString(R.string.req_trans)
        } else {
            nibssQrBottomSheetDialogBinding.progress.visibility = View.GONE
            if (viewModel.stillHasRetryAttempts)
                startNibssReQueryTimer()
        }
    }
}

Countdown Timer

private fun startNibssReQueryTimer() {
    val timer = object : CountDownTimer(4000, 1000) {
        override fun onTick(millisUntilFinished: Long) {
            nibssQrBottomSheetDialogBinding.scanToPay.text = getString(
                R.string.requerying_qr_transaction,
                millisUntilFinished / 1000
            )
        }
        override fun onFinish() {}
    }
    timer.start()
}

QR Payment Service

The QR payment API endpoint:
QrPaymentService.kt
interface QrPaymentService {
    @POST("contactlessQr")
    fun payWithQr(
        @Body payWithQrRequest: PayWithQrRequest,
    ): Single<Response<String>>
}

ViewModel Integration

ContactQrPaymentViewModel

ContactQrPaymentViewModel.kt
fun paymentWithQr(payWithQrRequest: PayWithQrRequest) =
    contactlessQrPaymentRepository.payWithQr(payWithQrRequest)
        .flatMap {
            if (it.isSuccessful) {
                savePaymentWithQrResponse(it.body().toString())
                Single.just(Resource.success(it.body()))
            } else {
                Single.just(Resource.error(it.errorBody()))
            }
        }

private fun savePaymentWithQrResponse(data: String) {
    Prefs.putString(AppConstants.PAYMENT_WITH_QR_STRING, gson.toJson(data))
}

Error Handling

Error: “An error occurred while fetching QR”Cause: API error, network issues, or invalid merchant configurationSolution: Verify merchant credentials and network connectivity
Error: “Too many attempts without response”Cause: Customer did not complete payment within 15 polling attempts (60 seconds)Solution: Generate a new QR code and retry the transaction
Error: Amount validation failsCause: Empty or non-numeric amount enteredSolution: Enter a valid numeric amount

Best Practices

Amount Validation

Always validate amount before generating QR to avoid wasted API calls

Loading States

Show progress indicators during QR generation and polling

Timeout Handling

Implement appropriate timeouts for NIBSS polling (15 attempts max)

Error Messages

Provide clear feedback when QR generation or payment verification fails
The QR code is encoded as a Base64 string in the API response and decoded to a Bitmap for display using Android’s BitmapFactory.

Build docs developers (and LLMs) love