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