Узнайте, как адаптировать ваше платежное приложение Android для работы с веб-платежами и обеспечить лучший пользовательский интерфейс для клиентов.
Опубликовано: 5 мая 2020 г., Последнее обновление: 27 мая 2025 г.
API запроса платежа приносит в веб встроенный интерфейс на основе браузера, который позволяет пользователям вводить требуемую платежную информацию проще, чем когда-либо прежде. API также может вызывать платежные приложения, специфичные для платформы.
По сравнению с использованием только Android Intents, веб-платежи обеспечивают лучшую интеграцию с браузером, безопасностью и пользовательским интерфейсом:
- Платежное приложение запускается как модальное окно в контексте сайта продавца.
- Реализация дополняет ваше существующее платежное приложение, позволяя вам воспользоваться преимуществами вашей пользовательской базы.
- Подпись платежного приложения проверяется для предотвращения сторонних загрузок .
- Платежные приложения могут поддерживать несколько способов оплаты.
- Можно интегрировать любой способ оплаты, например криптовалюту, банковские переводы и т. д. Платежные приложения на устройствах Android могут даже интегрировать методы, требующие доступа к аппаратному чипу на устройстве.
Для внедрения веб-платежей в платежном приложении Android необходимо выполнить четыре шага:
- Позвольте продавцам узнать о вашем платежном приложении.
- Сообщите продавцу, если у клиента есть зарегистрированный платежный инструмент (например, кредитная карта), готовый к оплате.
- Позвольте клиенту произвести оплату.
- Проверьте сертификат подписи вызывающего абонента.
Чтобы увидеть веб-платежи в действии, посмотрите демо-версию android-web-payment .
Шаг 1: Позвольте продавцам узнать о вашем платежном приложении
 Задайте свойство related_applications в манифесте веб-приложения в соответствии с инструкциями в разделе Настройка способа оплаты .
Чтобы продавец мог использовать ваше платежное приложение, ему необходимо использовать API запроса платежа и указать поддерживаемый вами способ оплаты с помощью идентификатора способа оплаты .
Если у вас есть идентификатор способа оплаты, уникальный для вашего платежного приложения, вы можете настроить собственный манифест способа оплаты, чтобы браузеры могли обнаружить ваше приложение.
Шаг 2: Сообщите продавцу, есть ли у клиента зарегистрированный инструмент, готовый к оплате.
 Торговец может вызвать hasEnrolledInstrument() чтобы узнать, может ли клиент совершить платеж . Вы можете реализовать IS_READY_TO_PAY как службу Android, чтобы ответить на этот запрос.
 AndroidManifest.xml
 Объявите свою услугу с помощью фильтра намерений с действием org.chromium.intent.action.IS_READY_TO_PAY .
<service
  android:name=".SampleIsReadyToPayService"
  android:exported="true">
  <intent-filter>
    <action android:name="org.chromium.intent.action.IS_READY_TO_PAY" />
  </intent-filter>
</service>
 Служба IS_READY_TO_PAY является необязательной. Если в платежном приложении нет такого обработчика намерений, то веб-браузер предполагает, что приложение всегда может совершать платежи.
АЙДЛ
 API для сервиса IS_READY_TO_PAY определено в AIDL. Создайте два файла AIDL со следующим содержимым:
org/chromium/IsReadyToPayServiceCallback.aidl
package org.chromium;
interface IsReadyToPayServiceCallback {
    oneway void handleIsReadyToPay(boolean isReadyToPay);
}
org/chromium/IsReadyToPayService.aidl
package org.chromium;
import org.chromium.IsReadyToPayServiceCallback;
interface IsReadyToPayService {
    oneway void isReadyToPay(IsReadyToPayServiceCallback callback, in Bundle parameters);
}
 Реализация IsReadyToPayService
 Простейшая реализация IsReadyToPayService показана в следующем примере: 
Котлин
class SampleIsReadyToPayService : Service() {
    private val binder = object : IsReadyToPayService.Stub() {
        override fun isReadyToPay(callback: IsReadyToPayServiceCallback?, parameters: Bundle?) {
            callback?.handleIsReadyToPay(true)
        }
    }
    override fun onBind(intent: Intent?): IBinder? {
        return binder
    }
}
Ява
import org.chromium.IsReadyToPayService;
public class SampleIsReadyToPayService extends Service {
    private final IsReadyToPayService.Stub mBinder =
        new IsReadyToPayService.Stub() {
            @Override
            public void isReadyToPay(IsReadyToPayServiceCallback callback, Bundle parameters) {
                if (callback != null) {
                    callback.handleIsReadyToPay(true);
                }
            }
        };
    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }
}
Ответ
 Служба может отправить свой ответ с помощью метода handleIsReadyToPay(Boolean) . 
Котлин
callback?.handleIsReadyToPay(true)
Ява
if (callback != null) {
    callback.handleIsReadyToPay(true);
}
Разрешение
 Вы можете использовать Binder.getCallingUid() чтобы проверить, кто является звонящим. Обратите внимание, что вам нужно сделать это в методе isReadyToPay , а не в методе onBind , поскольку ОС Android может кэшировать и повторно использовать соединение службы, что не запускает метод onBind() . 
Котлин
override fun isReadyToPay(callback: IsReadyToPayServiceCallback?, parameters: Bundle?) {
    try {
        val untrustedPackageName = parameters?.getString("packageName")
        val actualPackageNames = packageManager.getPackagesForUid(Binder.getCallingUid())
        // ...
Ява
@Override
public void isReadyToPay(IsReadyToPayServiceCallback callback, Bundle parameters) {
    try {
        String untrustedPackageName = parameters != null
                ? parameters.getString("packageName")
                : null;
        String[] actualPackageNames = packageManager.getPackagesForUid(Binder.getCallingUid());
        // ...
 Всегда проверяйте входные параметры на null при получении вызовов Inter-Process Communication (IPC). Это особенно важно, поскольку различные версии или форки ОС Android могут вести себя непредсказуемым образом и приводить к ошибкам, если их не обрабатывать.
 Хотя packageManager.getPackagesForUid() обычно возвращает один элемент, ваш код должен обрабатывать необычный сценарий, когда вызывающий использует несколько имен пакетов. Это гарантирует, что ваше приложение останется надежным.
Информацию о том, как проверить правильность подписи вызывающего пакета, см. в разделе Проверка сертификата подписи вызывающего абонента .
Параметры
 parameters Bundle были добавлены в Chrome 139. Их всегда следует проверять на наличие null .
 В пакете parameters сервису передаются следующие параметры:
-  packageName
-  methodNames
-  methodData
-  topLevelOrigin
-  paymentRequestOrigin
-  topLevelCertificateChain
 packageName был добавлен в Chrome 138. Вы должны проверить этот параметр с помощью Binder.getCallingUid() перед использованием его значения. Эта проверка необходима, поскольку пакет parameters находится под полным контролем вызывающей стороны, в то время как Binder.getCallingUid() контролируется ОС Android.
 topLevelCertificateChain имеет null в WebView и на веб-сайтах, не являющихся https, которые обычно используются для локального тестирования, например http://localhost .
Шаг 3: Позвольте клиенту совершить платеж
 Торговец вызывает show() для запуска платежного приложения , чтобы клиент мог совершить платеж. Платежное приложение вызывается с помощью намерения Android PAY с информацией о транзакции в параметрах намерения.
 Платежное приложение отвечает с помощью methodName и details , которые являются специфическими для платежного приложения и непрозрачны для браузера. Браузер преобразует строку details в словарь JavaScript для продавца с помощью десериализации строки JSON, но не обеспечивает никакой валидности за пределами этого. Браузер не изменяет details ; значение этого параметра передается напрямую продавцу.
 AndroidManifest.xml
 Действие с фильтром намерения PAY должно иметь тег <meta-data> , который определяет идентификатор способа оплаты по умолчанию для приложения .
 Для поддержки нескольких способов оплаты добавьте тег <meta-data> с ресурсом <string-array> . 
<activity
  android:name=".PaymentActivity"
  android:theme="@style/Theme.SamplePay.Dialog">
  <intent-filter>
    <action android:name="org.chromium.intent.action.PAY" />
  </intent-filter>
  <meta-data
    android:name="org.chromium.default_payment_method_name"
    android:value="https://bobbucks.dev/pay" />
  <meta-data
    android:name="org.chromium.payment_method_names"
    android:resource="@array/chromium_payment_method_names" />
</activity>
android:resource должен быть списком строк, каждая из которых должна быть допустимым абсолютным URL-адресом со схемой HTTPS, как показано здесь. 
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="chromium_payment_method_names">
        <item>https://alicepay.com/put/optional/path/here</item>
        <item>https://charliepay.com/put/optional/path/here</item>
    </string-array>
</resources>
Параметры
 Следующие параметры передаются в действие как дополнительные параметры Intent :
-  methodNames
-  methodData
-  merchantName
-  topLevelOrigin
-  topLevelCertificateChain
-  paymentRequestOrigin
-  total
-  modifiers
-  paymentRequestId
-  paymentOptions
- shippingOptions
Котлин
val extras: Bundle? = getIntent()?.extras
Ява
Bundle extras = getIntent() != null ? getIntent().getExtras() : null;
МетодИмена
 Названия используемых методов. Элементы — это ключи в словаре methodData . Это методы, которые поддерживает платежное приложение. 
Котлин
val methodNames: List<String>? = extras.getStringArrayList("methodNames")
Ява
List<String> methodNames = extras.getStringArrayList("methodNames");
 methodData
 Сопоставление каждого из methodNames с methodData . 
Котлин
val methodData: Bundle? = extras.getBundle("methodData")
Ява
Bundle methodData = extras.getBundle("methodData");
 merchantName
 Содержимое HTML-тега <title> на странице оформления заказа у продавца (контекст просмотра верхнего уровня браузера). 
Котлин
val merchantName: String? = extras.getString("merchantName")
Ява
String merchantName = extras.getString("merchantName");
 topLevelOrigin
 Происхождение продавца без схемы (Происхождение без схемы контекста просмотра верхнего уровня). Например, https://mystore.com/checkout передается как mystore.com . 
Котлин
val topLevelOrigin: String? = extras.getString("topLevelOrigin")
Ява
String topLevelOrigin = extras.getString("topLevelOrigin");
 topLevelCertificateChain
 Цепочка сертификатов продавца (цепочка сертификатов контекста просмотра верхнего уровня). Значение равно null для WebView, localhost или файла на диске. Каждый Parcelable — это Bundle с ключом certificate и значением массива байтов. 
Котлин
val topLevelCertificateChain: Array<Parcelable>? =
        extras.getParcelableArray("topLevelCertificateChain")
val list: List<ByteArray>? = topLevelCertificateChain?.mapNotNull { p ->
    (p as Bundle).getByteArray("certificate")
}
Ява
Parcelable[] topLevelCertificateChain =
        extras.getParcelableArray("topLevelCertificateChain");
if (topLevelCertificateChain != null) {
    for (Parcelable p : topLevelCertificateChain) {
        if (p != null && p instanceof Bundle) {
            ((Bundle) p).getByteArray("certificate");
        }
    }
}
 paymentRequestOrigin
 Источник без схемы контекста просмотра iframe, вызвавшего new PaymentRequest(methodData, details, options) в JavaScript. Если конструктор был вызван из контекста верхнего уровня, то значение этого параметра равно значению параметра topLevelOrigin . 
Котлин
val paymentRequestOrigin: String? = extras.getString("paymentRequestOrigin")
Ява
String paymentRequestOrigin = extras.getString("paymentRequestOrigin");
 total
Строка JSON, представляющая общую сумму транзакции.
Котлин
val total: String? = extras.getString("total")
Ява
String total = extras.getString("total");
Вот пример содержимого строки:
{"currency":"USD","value":"25.00"}
 modifiers
 Вывод JSON.stringify(details.modifiers) , где details.modifiers содержат только supportedMethods , data и total .
 paymentRequestId
 Поле PaymentRequest.id , которое приложения "push-payment" должны связать с состоянием транзакции. Веб-сайты торговцев будут использовать это поле для запроса приложений "push-payment" о состоянии транзакции вне диапазона. 
Котлин
val paymentRequestId: String? = extras.getString("paymentRequestId")
Ява
String paymentRequestId = extras.getString("paymentRequestId");
Ответ
 Действие может отправить свой ответ обратно через setResult с RESULT_OK . 
Котлин
setResult(Activity.RESULT_OK, Intent().apply {
    putExtra("methodName", "https://bobbucks.dev/pay")
    putExtra("details", "{\"token\": \"put-some-data-here\"}")
})
finish()
Ява
Intent result = new Intent();
Bundle extras = new Bundle();
extras.putString("methodName", "https://bobbucks.dev/pay");
extras.putString("details", "{\"token\": \"put-some-data-here\"}");
result.putExtras(extras);
setResult(Activity.RESULT_OK, result);
finish();
В качестве дополнительных параметров Intent необходимо указать два параметра:
-  methodName: Имя используемого метода.
-  details: строка JSON, содержащая информацию, необходимую продавцу для завершения транзакции. Если success равенtrue, тоdetailsдолжны быть построены таким образом, чтобыJSON.parse(details)был успешным. Если нет данных, которые нужно вернуть, то эта строка может быть"{}", которую веб-сайт продавца получит как пустой словарь JavaScript.
 Вы можете передать RESULT_CANCELED если транзакция не была завершена в платежном приложении, например, если пользователь не ввел правильный PIN-код для своей учетной записи в платежном приложении. Браузер может позволить пользователю выбрать другое платежное приложение. 
Котлин
setResult(Activity.RESULT_CANCELED)
finish()
Ява
setResult(Activity.RESULT_CANCELED);
finish();
 Если результат действия платежного ответа, полученного от вызванного платежного приложения, установлен на RESULT_OK , то Chrome проверит непустое methodName и details в своих дополнительных данных. Если проверка не пройдена, Chrome вернет отклоненное обещание из request.show() с одним из следующих сообщений об ошибках, с которыми сталкивается разработчик: 
'Payment app returned invalid response. Missing field "details".'
'Payment app returned invalid response. Missing field "methodName".'
Разрешение
 Действие может проверить вызывающую сторону с помощью метода getCallingPackage() . 
Котлин
val caller: String? = callingPackage
Ява
String caller = getCallingPackage();
Последний шаг — проверка сертификата подписи вызывающего абонента, чтобы подтвердить, что вызывающий пакет имеет правильную подпись.
Шаг 4: Проверьте сертификат подписи вызывающего абонента
 Вы можете проверить имя пакета вызывающего с помощью Binder.getCallingUid() в IS_READY_TO_PAY и с помощью Activity.getCallingPackage() в PAY . Чтобы действительно убедиться, что вызывающий — это браузер, который вы имеете в виду, вы должны проверить его сертификат подписи и убедиться, что он соответствует правильному значению.
 Если вы ориентируетесь на API уровня 28 и выше и выполняете интеграцию с браузером, имеющим единый сертификат подписи, вы можете использовать PackageManager.hasSigningCertificate() . 
Котлин
val packageName: String = … // The caller's package name
val certificate: ByteArray = … // The correct signing certificate
val verified = packageManager.hasSigningCertificate(
        callingPackage,
        certificate,
        PackageManager.CERT_INPUT_SHA256
)
Ява
String packageName = … // The caller's package name
byte[] certificate = … // The correct signing certificate
boolean verified = packageManager.hasSigningCertificate(
        callingPackage,
        certificate,
        PackageManager.CERT_INPUT_SHA256);
 PackageManager.hasSigningCertificate() предпочтительнее для браузеров с одним сертификатом, поскольку он правильно обрабатывает ротацию сертификатов. (У Chrome один сертификат подписи.) Приложения, имеющие несколько сертификатов подписи, не могут их ротировать.
 Если вам нужна поддержка API уровней 27 и ниже или если вам нужно работать с браузерами с несколькими сертификатами подписи, вы можете использовать PackageManager.GET_SIGNATURES . 
Котлин
val packageName: String = … // The caller's package name
val expected: Set<String> = … // The correct set of signing certificates
val packageInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES)
val sha256 = MessageDigest.getInstance("SHA-256")
val actual = packageInfo.signatures.map {
    SerializeByteArrayToString(sha256.digest(it.toByteArray()))
}
val verified = actual.equals(expected)
Ява
String packageName  = … // The caller's package name
Set<String> expected = … // The correct set of signing certificates
PackageInfo packageInfo =
        packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
Set<String> actual = new HashSet<>();
for (Signature it : packageInfo.signatures) {
    actual.add(SerializeByteArrayToString(sha256.digest(it.toByteArray())));
}
boolean verified = actual.equals(expected);
Отлаживать
Используйте следующую команду для просмотра ошибок или информационных сообщений:
adb logcat | grep -i pay
 
 
        
        