How to accept payments in a mobile application: tokenization, NFC, optical scanning and other goodies in one SDK

Tomcat

Professional
Messages
2,376
Reputation
4
Reaction score
406
Points
83
I have already told you earlier, using the Android SDK as an example, how to not limit yourself to a frame and WebView, build a native form for accepting payments by bank card into a mobile application, and at the same time not fall under a PCI DSS audit. Since then, our SDK has expanded quite significantly and the following functionality has been added to the usual card entry form in Android and iOS:
- React Native library for Android and iOS
- customization of the layout of the form with card details
- optical card scanning function
- acceptance of contactless payments in Android via NFC technologies
In this publication, I will tell you what can be done with payments in mobile applications, what life hacks and pitfalls there are, and finally, I will give an example code for a demo application and tell you how to write off a card debt from a friend using the NFC reader of your smartphone.

Case 1. Linking a client card to the backend for regular debits or 1-click payments.​


It is important to understand here that if your backend is not PCI DSS certified, then you cannot store the card number and its expiration date in your database. Therefore, before linking the card ID to the client account, the card must first be tokenized. To do this, you need to make the first payment through the mobile application with the participation of the client, and preferably with 3D-Secure, blocking a small amount on the card, for example 1 unit of currency. 3D-secure in this case is necessary, first of all, to protect yourself, as a retail outlet, from financial claims (chargebacks) for future recurring write-offs, and secondly, to improve conversion, as, for example, with cards from Sberbank in Russia and Privatbank in In Ukraine, in most cases, a transaction without 3D-Secure will not go through.
So, in order to receive a card token, you need to pass the requiredRecToken and verification parameters (for more details on how to create a mobile application, see the article, the link to which I indicated at the beginning, as well as in the code of the demo application on github):
Code:
order.setRequiredRecToken(true)

order.setVerification(true)

The requiredRecToken parameter requires that the card token be returned upon successful authorization of the card, and verification - that there is no need to write off funds from the card, but simply block them and then return them (the payment gateway returns them automatically).

In response, the payment gateway will return the parameters recToken - the card token, recTokenLifeTime - the validity period of the token (essentially the validity period of the card) and maskedCard - a masked card number that must be linked to the token in the backend for further display to the client when choosing a payment method. Now, having a card token, you can call the token debit
method via the server-to-server API at any time, upon the client’s request or when payment is due, and debit the required amount.

Pitfalls: According to our statistics, a fairly significant portion of cardholders are unable to pay via 3DSecure on a mobile device for a number of reasons beyond the gateway and the device: - SMS may not arrive, or the user, switching between the SMS application and yours, has lost the input form password 3D-Secure, since it opens in WebView or the system browser - the layout of the bank's 3D-Secure page on a smartphone or tablet came in handy (banks very rarely adapt such pages) - the bank's web server has disabled support for the insecure TSL 1.0 protocol, which makes 3D-Secure Secure is not available for Android version <4.1 Life hack: On the payment gateway we can enable/disable 3D-Secure on the fly, and if the client still fails to pay, we adapt to him and try to make the payment without a 3D-Secure password. It is also worth remembering that if you save tokens from one payment provider in your system, then you will not be able to use them on another provider, unless the providers agree among themselves on the migration of tokens, which, in principle, has already happened several times in our practice.

Case 2. Customizing the layout of the card number entry form.​


Often there is a need to place fields for entering the card number, expiration date and cvv2 in a different sequence than is provided by the standard layout in the SDK. But due to PCI DSS requirements, you cannot simply replace the card number input field with a standard EditText component. For these purposes, we have developed a flexible layout. Flexible layout inherits the styles of your mobile application and allows you to arrange form elements in any sequence and in any design, while preventing accidental transfer of card data to your backend.

To organize card input in the SDK, there are two mechanisms:
CardInputView - a ready-made view for use;
CardInputLayout is just a layout wrapper for building a view in your own markup style.

Essentially CardInputView = CardInputLayout + CardNumberEdit + CardExpMmEdit + CardExpYyEdit + CardCvvEdit.
A simplified CardInputView structure in XML can be written like this:

Code:
<com.cloudipsp.android.CardInputLayout>
  <com.cloudipsp.android.CardNumberEdit/>
  <LinearLayout  android:orientation="horizontal">
    <com.cloudipsp.android.CardExpMmEdit />
    <com.cloudipsp.android.CardExpYyEdit />
  </LinearLayout>
  <com.cloudipsp.android.CardCvvEdit />
<com.cloudipsp.android.CardInputLayout>

Therefore, you can absolutely freely customize and arrange input elements according to your imagination. There is only one rule that must be followed - each of the input elements (CardNumberEdit, CardExpMmEdit, CardExpYyEdit, CardCvvEdit) must be in the CardInputLayout once, and the View nesting level does not matter.

Pitfalls:
When customizing input fields, it's worth remembering:
- cvv2 can be either 3 or 4 characters long
- the card number can be from 14 to 19 characters
- you can achieve the most precise customization to your design by forking the SDK and making changes already in your layout implementation (this is not forbidden to do, unless you start passing card details through your backend). But by forking, you lose support for SDK updates from the gateway and the integration of new features.

Lifehack:
You can often find inputs on the form for entering card details for entering the first and last name of the cardholder and his ZIP code. For payments across the CIS, there is no practical need to do this in 99% of cases - only some banks in the USA, Canada and the UK support this technology, which is called Address Verification System, and for the verification to work, it must be supported by both the acquiring bank and the bank. issuer

59d54f188816b320391879.png


Case 3. Connecting the ability to scan a card via camera and NFC​


The optical card scanning function is implemented for Android in the android-sdk-optical library, for iOS in the CloudipspOptical library using card.io SDK.
NFC scanning is implemented using the android-sdk-nfc and react-native-cloudipsp-nfc libraries and is available only for Android. Although Apple has opened up the ability for third-party developers to read RFID tags starting with iOS 11+ , reading EMV tags from bank cards still remains unavailable.

Example demo application for using NFC

Code:
package com.cloudipsp.nfcexample;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Patterns;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.Toast;

import com.cloudipsp.android.Card;
import com.cloudipsp.android.CardInputView;
import com.cloudipsp.android.Cloudipsp;
import com.cloudipsp.android.CloudipspWebView;
import com.cloudipsp.android.Currency;
import com.cloudipsp.android.Order;
import com.cloudipsp.android.Receipt;
import com.cloudipsp.nfc.NfcCardBridge;

public class MainActivity extends Activity implements View.OnClickListener {
private static final int MERCHANT_ID = 1396424;

private EditText editAmount;
private Spinner spinnerCcy;
private EditText editEmail;
private EditText editDescription;
private CardInputView cardInput;
private CloudipspWebView webView;

private Cloudipsp cloudipsp;
private NfcCardBridge nfcCardBridge;

@override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

nfcCardBridge = new NfcCardBridge(this);

findViewById(R.id.btn_amount).setOnClickListener(this);
editAmount = (EditText) findViewById(R.id.edit_amount);
spinnerCcy = (Spinner) findViewById(R.id.spinner_ccy);
editEmail = (EditText) findViewById(R.id.edit_email);
editDescription = (EditText) findViewById(R.id.edit_description);
cardInput = (CardInputView) findViewById(R.id.card_input);
cardInput.setHelpedNeeded(true);
findViewById(R.id.btn_pay).setOnClickListener(this);

webView = (CloudipspWebView) findViewById(R.id.web_view);
cloudipsp = new Cloudipsp(MERCHANT_ID, webView);

spinnerCcy.setAdapter(new ArrayAdapter<Currency>(this, android.R.layout.simple_spinner_item, Currency.values()));

if (savedInstanceState == null) {
processIntent(getIntent());
        }
    }

@override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_amount:
 fillTest();
 break;
case R.id.btn_pay:
 processPay();
 break;
        }
    }

private void fillTest() {
editAmount.setText("1");
editEmail.setText("[email][email protected][/email]");
editDescription.setText("test payment");
    }

private void processPay() {
editAmount.setError(null);
editEmail.setError(null);
editDescription.setError(null);

final int amount;
 try {
amount = Integer.valueOf(editAmount.getText().toString());
} catch (Exception e) {
editAmount.setError(getString(R.string.e_invalid_amount));
 return;
        }

final String email = editEmail.getText().toString();
final String description = editDescription.getText().toString();
if (TextUtils.isEmpty(email) || !Patterns.EMAIL_ADDRESS.matcher(email).matches()) {
editEmail.setError(getString(R.string.e_invalid_email));
} else if (TextUtils.isEmpty(description)) {
editDescription.setError(getString(R.string.e_invalid_description));
} else {
final Currency currency = (Currency) spinnerCcy.getSelectedItem();
final Order order = new Order(amount, currency, "vb_" + System.currentTimeMillis(), description, email);
order.setLang(Order.Lang.ru);
final Card card;
if (nfcCardBridge.hasCard()) {
card = nfcCardBridge.getCard(order);
cardInput.display(null);
} else {
card = cardInput.confirm();
            }

cloudipsp.pay(card, order, new Cloudipsp.PayCallback() {
@override
public void onPaidProcessed(Receipt receipt) {
Toast.makeText(MainActivity.this, "Paid " + receipt.status.name() + "\nPaymentId:" + receipt.paymentId, Toast.LENGTH_LONG).show();
                }

@override
public void onPaidFailure(Cloudipsp.Exception e) {
if (e instanceof Cloudipsp.Exception.Failure) {
Cloudipsp.Exception.Failure f = (Cloudipsp.Exception.Failure) e;

Toast.makeText(MainActivity.this, "Failure\nErrorCode: " +
f.errorCode + "\nMessage: " + f.getMessage() + "\nRequestId: " + f.requestId, Toast.LENGTH_LONG).show();
} else if (e instanceof Cloudipsp.Exception.NetworkSecurity) {
Toast.makeText(MainActivity.this, "Network security error: " + e.getMessage(), Toast.LENGTH_LONG).show();
} else if (e instanceof Cloudipsp.Exception.ServerInternalError) {
Toast.makeText(MainActivity.this, "Internal server error: " + e.getMessage(), Toast.LENGTH_LONG).show();
} else if (e instanceof Cloudipsp.Exception.NetworkAccess) {
Toast.makeText(MainActivity.this, "Network error", Toast.LENGTH_LONG).show();
} else {
Toast.makeText(MainActivity.this, "Payment Failed", Toast.LENGTH_LONG).show();
                    }
e.printStackTrace();
                }
            });
        }
    }

@override
public void onBackPressed() {
if (webView.waitingForConfirm()) {
webView.skipConfirm();
} else {
super.onBackPressed();
        }
    }

@override
public void onNewIntent(Intent intent) {
super.onNewIntent(intent);

processIntent(intent);
    }

private void processIntent(Intent intent) {
if (nfcCardBridge.readCard(intent)) {
Toast.makeText(this, "NFC Card read success", Toast.LENGTH_LONG).show();
nfcCardBridge.displayCard(cardInput);
        }
    }
}

It differs from the usual implementation by the presence of NfcCardBridge and attaching an Intent to it to wait for an event that the card has been read (readCard).

Pitfalls:
Although the card is read via NFC, the protocol for financial authorization of the card is still the usual card not present. Those. For this functionality to fully work, the card must be open for online payments.

Life hack:
By writing a simple application, you can use it to transfer funds from someone else’s card to yours by holding someone else’s card to your phone. For example, this can be convenient if you need to write off a small amount from a friend for a card debt. On the one hand, it will be practical and convenient, on the other hand, it will be quite impressive. In order to use the card-to-card transfer service, you will need to first register on the website of the Fondy payment platform and link the bank card to which the funds will be transferred to your financial settings. For security purposes, the amount that can be written off via NFC without 3D-Secure support can be no more than the equivalent of $4.
 
Top