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

    I already told earlier on the example of the Android SDK, how, not limited to the frame and WebView, embed the native form of accepting payments by bank card in a mobile application, and at the same time not fall under the PCI DSS audit. Since then, our SDK has expanded quite significantly, and the following functionality has been added to the usual card input form in Android and iOS:
    - React Native library for Android and iOS
    - customization of layout layout forms with card details
    - optical scan of the card function
    - acceptance of contactless payments in Android by NFC technology
    In this publication, I will tell you what you can generally do with payments in mobile applications, what life hacks and pitfalls are, and finally I will give an example of a demo application code and tell you how to write off card debt from a friend using the NFC reader of my smartphone.


    Case 1. We attach the client’s card to the backend for regular debits or payments in 1 click.


    It is important to understand that if your backend is not certified according to PCI DSS, then you can not store the card number and its expiration date in your database. Therefore, before binding the card ID to the client’s account, you must first tokenize the card. 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, by blocking a small amount on the card, for example 1 unit of currency. In this case, 3D-secure is necessary in the first place in order to protect yourself, as a point of sale, from financial claims (chargebacks) for future recurring charges, and secondly, in order to improve the conversion, for example, using Sberbank in Russia and Privatbank cards in Ukraine, in most cases, a transaction without 3D-Secure will fail.
    So, in order to get the 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 demo application code on github):
    order.setRequiredRecToken(true)

    order.setVerification(true)

    The requiredRecToken parameter requires the return of the card token upon successful authorization of the card, and verification - that funds are not required to be deducted from the card, but it is enough to block them and then return them (the payment gateway returns them automatically).
    In response, the payment gateway will return the parameters recToken - card token, recTokenLifeTime - token expiration date (essentially the card expiration date) and maskedCard - masked card number, which must be linked in the backend to the token for further display to the client when choosing a payment method.
    Now, having a card token, you can at any time at the request of the client or when the payment is due, call the token debit method через server-to-server API и списать необходимую сумму.
    Подводные камни:
    По нашей статистике у довольно значимой части картодержателей не получается оплатить через 3DSecure на мобильном устройстве по ряду причин, от него и шлюза не зависящих:
    — может не приходить SMS, или пользователь переключаясь между SMS-приложением и вашим, потерял форму с вводом пароля 3D-Secure, так как она открывается в WebView или системном браузере
    — полезла верстка 3D-Secure страницы банка на смартфоне или планшете (банки очень редко адаптируют такие страницы)
    — веб-сервер банка отключил поддержку небезопасного протокола TSL 1.0, что делает 3D-Secure недоступным для Android версии <4.1
    Лайфхак:
    We are able to enable / disable 3D-Secure on-line on the payment gateway, and if the client still cannot pay, we adapt to it and try to make the payment without a 3D-Secure password.
    It is also worth remembering that if you save the tokens of one payment provider in your system, then you won’t be able to use them on another provider, unless the providers agree on the migration of tokens, which in principle has already happened several times.

    Case 2. We customize the layout of the form for entering the card number.


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

    There are two mechanisms for organizing card input in the SDK:
    CardInputView - a ready view for use;
    CardInputLayout is just a layout wrapper for creating a view in its own markup style.

    Essentially CardInputView = CardInputLayout + CardNumberEdit + CardExpMmEdit + CardExpYyEdit + CardCvvEdit.
    The simplified structure of CardInputView in XML can be drunk like this:



    Therefore, you can absolutely freely customize and arrange input elements for as long as there is enough imagination. There is only one rule that must be observed - each of the input elements (CardNumberEdit, CardExpMmEdit, CardExpYyEdit, CardCvvEdit) must be in CardInputLayout once, and the View nesting level does not matter.
    Here's how it might look:

    Pitfalls: When customizing
    input fields, remember:
    - cvv2 can be either 3 or 4 characters long
    - card number can be from 14 to 19 characters
    - you can achieve the most accurate customization to your design by forking the SDK and making changes to your layout implementation (this is not forbidden to do if you do not start to pass the card details through your backend). But having made a fork, you lose support for SDK updates from the gateway and the integration of new features
    Lifehack:
    Often you can find input on the card details input form for entering the cardholder’s name and surname and its ZIP code. For payments in 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 in order for the check to work, it must be supported by both the acquiring bank and the bank- issuer


    Case 3. We connect the ability to scan cards through the 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 the opportunity for third-party developers to read RFID tags starting with iOS 11+ , reading EMV tags from bank cards is still unavailable.

    Sample Demo for Using NFC
    
    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(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("test@test.com");
            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 hanging Intent on it to wait for the event that the card has been read (readCard)
    Pitfalls:
    Although the card is read through NFC, the card’s financial authorization protocol is still the usual card not present. Those. For the full functionality of this functionality, the card must be open for payments on the Internet.
    Lifehack:
    By writing a simple application, you can use it to transfer funds from someone else's card to your own, bringing someone else's card to your phone. For example, it may be convenient if you need to write off a small amount from a friend as a card debt. On the one hand, it will be practical and convenient, on the other - quite spectacular. In order to use the card-to-card transfer service, you will need to first register on the site of the Fondy payment platform and link the bank card to which funds will be transferred to your financial settings. For security purposes, the amount that can be debited via NFC without 3D-Secure support can be no more than the equivalent of $ 4.

    Also popular now: