Как принимать платежи в мобильном приложении: токенизация, NFC, оптическое сканирование и другие плюшки в одном SDK

    Я уже рассказывал ранее на примере Android SDK, как не ограничиваясь фреймом и WebView, встроить нативную форму приема платежей по банковской карте в мобильное приложение, и при этом не попасть под аудит PCI DSS. С тех пор наше SDK довольно существенно расширилось и к обычной форме ввода карты в Android и iOS добавился такой функционал:
    — React Native библиотека для Android и iOS
    — кастомизация верстки layout формы с реквизитами карты
    — функция оптического сканирования карты
    — прием бесконтактных платежей в Android по технологии NFC
    В этой публикации я расскажу что вообще можно делать с платежами в мобильных приложениях, какие есть лайфхаки и подводные камни, и напоследок приведу пример кода демо-приложения и расскажу, как списать карточный долг с друга при помощи NFC ридера своего смартфона.


    Кейс 1. Привязываем карту клиента к бэкенду для регулярных списаний или платежей в 1 клик.


    Тут важно понимать, что если ваш бэкенд не сертифицирован по PCI DSS, то номер карты и ее срок действия вы не можете хранить в своей базе данных. Поэтому, прежде чем привязать идентификатор карты к аккаунту клиента, необходимо сначала карту токенизировать. Для этого вам необходимо осуществить через мобильное приложение первый платеж с участием клиента, и желательно с 3D-Secure, заблокировав на карте небольшую сумму, например 1 единицу валюты. 3D-secure в данном случае необходим в первую очередь, чтобы обезопасить себя, как торговую точку, от финансовых претензий (чарджбеков) по будущим рекурентным списаниям, а во вторую очередь — чтобы улучшить конверсию, так как например по картам Сбербанка в России и Приватбанка в Украине в большинстве случаев транзакция без 3D-Secure не пройдет.
    Итак, чтобы получить токен карты, необходимо передать параметры requiredRecToken и verification (более подробно как создать мобильное приложение смотрите в статье, ссылку на которую я указал в начале, а также в коде демо-приложения на github):
    order.setRequiredRecToken(true)

    order.setVerification(true)

    Параметр requiredRecToken требует возвратить токен карты при успешной авторизации карты, а verification — что средства с карты списывать не нужно, а достаточно их заблокировать, а потом вернуть (платежный шлюз возвращает их автоматически).
    В ответ платежный шлюз вернет параметры recToken — токен карты, recTokenLifeTime — срок действия токена (по сути срок действия карты) и maskedCard — маскированный номер карты, который необходимо привязать в бекенде к токену для дальнейшего отображения клиенту при выборе способа оплаты.
    Теперь, имея токен карты вы можете в любой момент по требованию клиента или при наступлении срока оплаты, вызвать метод списания по токену через server-to-server API и списать необходимую сумму.
    Подводные камни:
    По нашей статистике у довольно значимой части картодержателей не получается оплатить через 3DSecure на мобильном устройстве по ряду причин, от него и шлюза не зависящих:
    — может не приходить SMS, или пользователь переключаясь между SMS-приложением и вашим, потерял форму с вводом пароля 3D-Secure, так как она открывается в WebView или системном браузере
    — полезла верстка 3D-Secure страницы банка на смартфоне или планшете (банки очень редко адаптируют такие страницы)
    — веб-сервер банка отключил поддержку небезопасного протокола TSL 1.0, что делает 3D-Secure недоступным для Android версии <4.1
    Лайфхак:
    Мы на платежном шлюзе умеем включать/отключать налету 3D-Secure, и если все-таки у клиента не получается оплатить, мы под него подстраиваемся, и пытаемся сделать оплату без 3D-Secure пароля.
    Также стоит помнить, что если вы сохраняете токены одного платежного провайдера в своей системе, то использовать их на другом провайдере уже не получится, разве что если провайдеры не договорятся между собой о миграции токенов, что в принципе в нашей практике уже было несколько раз.

    Кейс 2. Кастомизируем верстку формы ввода номера карты.


    Часто возникает необходимость разместить поля для ввода номера карты, срока действия и cvv2 в другой последовательности, чем это предусмотрено стандартным layout в SDK. Но из-за требований PCI DSS вы не можете просто взять, и заменить поле ввода номера карты на стандартный компонент EditText. Для этих целей мы разработали flexible layout. Flexible layout наследует стили вашего мобильного приложения и позволяет располагать элементы формы в любой последовательности и в любом дизайне и при этом предотвращает случайную передачу карточных данных на сторону вашего бекэнда.

    Для организации ввода карты в SDK есть два механизма:
    CardInputView — готовый view для использования;
    CardInputLayout — лишь layout wrapper для потроение view в собственном стиле разметки.

    По сути CardInputView = CardInputLayout + CardNumberEdit + CardExpMmEdit + CardExpYyEdit + CardCvvEdit.
    Упрощенную структуру CardInputView в XML можно запиться так:

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


    Следовательно можно абсолютно свободно кастомизировать и располагать элементы ввода на сколько хватит фантазии. Есть лишь одно правило которое нужно соблюдать — каждый из элементов ввода (CardNumberEdit,CardExpMmEdit,CardExpYyEdit,CardCvvEdit) должен быть в CardInputLayout один раз, при этом не играет роли уровень вложенности View.
    Вот как это может выглядеть:

    Подводные камни:
    Кастомизируя поля ввода стоит помнить:
    — cvv2 может быть длиной как 3, так и 4 символа
    — номер карты может быть от 14 до 19 символов
    — можно добиться максимально точной кастомизации к вашему дизайну, сделав форк SDK и внеся изменения уже в своей реализации layout (это не запрещено делать, если вы не начинаете пропускать реквизиты карты через свой бэкенд). Но сделав форк вы теряете поддержку обновлений SDK со стороны шлюза и интеграцию новых фич
    Лайфхак:
    Часто можно встретить на форме ввода реквизитов карты инпуты для ввода имени и фамилии картодержателя и его ZIP кода. Для платежей по СНГ нет практической необходимости это делать в 99% случаев — только некоторые банки США, Канады и Великобритании поддерживают эту технологию, которая называется Address Verification System, при этом чтобы проверка сработала, ее должны поддерживать как банк-эквайер, так и банк-эмитент


    Кейс 3. Подключаем возможность сканирования карты через камеру и NFC


    Функция оптического сканирования карты реализована для Android в библиотеке android-sdk-optical, для iOS в библиотеке CloudipspOptical с использованием card.io SDK.
    NFC сканирование реализовано при помощи библиотек android-sdk-nfc и react-native-cloudipsp-nfc и доступно только для Android. Хотя Apple и открыла начиная с версии iOS 11+ сторонним разработчикам возможность читать RFID метки, но чтение EMV тегов с банковских карт по прежнему остается недоступным.

    Пример демо-приложения для использования 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<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("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);
            }
        }
    }
    


    Отличается от обычной реализации наличием NfcCardBridge и навешиванием Intent на него для ожидания события, что карта прочитана (readCard)
    Подводные камни:
    Хотя считывание карты и выполняется посредством NFC, протоколом финансовой авторизации карты по-прежнему служит обычный card not present. Т.е. для полноценной работы этой функциональности, карта должна быть открыта для платежей в интернет.
    Лайфхак:
    Написав простое приложение, вы сможете использовать его для перевода средств с чужой карты на свою, поднеся чужую карту к телефону. Например это может быть удобно, если вам необходимо списать небольшую сумму с друга в счет карточного долга. С одной стороны это будет практично и удобно, с другой — вполне эффектно. Для того, чтобы воспользоваться сервисом переводов с карты на карту, необходимо будет предварительно зарегистрироваться на сайте платежной платформы Fondy и привязать банковскую карту, на которую будут поступать средства, в свои финансовые настройки. С целью обеспечения безопасности, сумма, которую можно списать посредством NFC без поддержки 3D-Secure может быть не более эквивалента $4.
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 0

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.