Я уже рассказывал ранее на примере 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.

Комментарии (0)