Узнайте, как адаптировать ваше платежное приложение 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