Руководство для разработчиков платежных приложений Android

Узнайте, как адаптировать ваше платежное приложение Android для работы с веб-платежами и обеспечить лучший пользовательский интерфейс для клиентов.

Опубликовано: 5 мая 2020 г., Последнее обновление: 27 мая 2025 г.

API запроса платежа приносит в веб встроенный интерфейс на основе браузера, который позволяет пользователям вводить требуемую платежную информацию проще, чем когда-либо прежде. API также может вызывать платежные приложения, специфичные для платформы.

Browser Support

  • Хром: 60.
  • Край: 15.
  • Firefox: за флагом.
  • Сафари: 11.1.

Source

Процесс оформления заказа с использованием платформенного приложения Google Pay, использующего веб-платежи.

По сравнению с использованием только Android Intents, веб-платежи обеспечивают лучшую интеграцию с браузером, безопасностью и пользовательским интерфейсом:

  • Платежное приложение запускается как модальное окно в контексте сайта продавца.
  • Реализация дополняет ваше существующее платежное приложение, позволяя вам воспользоваться преимуществами вашей пользовательской базы.
  • Подпись платежного приложения проверяется для предотвращения сторонних загрузок .
  • Платежные приложения могут поддерживать несколько способов оплаты.
  • Можно интегрировать любой способ оплаты, например криптовалюту, банковские переводы и т. д. Платежные приложения на устройствах Android могут даже интегрировать методы, требующие доступа к аппаратному чипу на устройстве.

Для внедрения веб-платежей в платежном приложении Android необходимо выполнить четыре шага:

  1. Позвольте продавцам узнать о вашем платежном приложении.
  2. Сообщите продавцу, если у клиента есть зарегистрированный платежный инструмент (например, кредитная карта), готовый к оплате.
  3. Позвольте клиенту произвести оплату.
  4. Проверьте сертификат подписи вызывающего абонента.

Чтобы увидеть веб-платежи в действии, посмотрите демо-версию 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