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

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

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

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

Browser Support

  • Chrome: 60.
  • Край: 15.
  • Firefox: за флагом.
  • Safari: 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
    }
}

Java

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)

Java

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())
        // ...

Java

@Override
public void isReadyToPay(IsReadyToPayServiceCallback callback, Bundle parameters) {
    try {
        String untrustedPackageName = parameters != null
                ? parameters.getString("packageName")
                : null;
        String[] actualPackageNames = packageManager.getPackagesForUid(Binder.getCallingUid());
        // ...

При получении вызовов межпроцессного взаимодействия (IPC) всегда проверяйте входные параметры на наличие null . Это особенно важно, поскольку разные версии или форки ОС Android могут вести себя непредсказуемо и приводить к ошибкам, если их не обрабатывать должным образом.

Хотя packageManager.getPackagesForUid() обычно возвращает один элемент, ваш код должен обрабатывать редкий сценарий, когда вызывающая сторона использует несколько имен пакетов. Это гарантирует надежность вашего приложения.

См. раздел «Проверка сертификата подписи вызывающего абонента» , чтобы узнать, как убедиться в наличии правильной подписи в пакете данных вызывающего абонента.

Параметры

parameters Bundle был добавлен в Chrome версии 139. Его всегда следует проверять на наличие null .

В пакете parameters сервису передаются следующие параметры:

  • packageName
  • methodNames
  • methodData
  • topLevelOrigin
  • paymentRequestOrigin
  • topLevelCertificateChain

Параметр packageName был добавлен в Chrome версии 138. Перед использованием значения этого параметра необходимо проверить его соответствие методу Binder.getCallingUid() . Эта проверка необходима, поскольку пакет parameters находится под полным контролем вызывающей стороны, в то время как Binder.getCallingUid() контролируется операционной системой Android.

В WebView и на сайтах без HTTPS, обычно используемых для локального тестирования, таких как http://localhost , topLevelCertificateChain null

Шаг 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

Java

Bundle extras = getIntent() != null ? getIntent().getExtras() : null;

methodNames

Названия используемых методов. Элементы являются ключами в словаре methodData . Это методы, которые поддерживает платежное приложение.

Котлин

val methodNames: List<String>? = extras.getStringArrayList("methodNames")

Java

List<String> methodNames = extras.getStringArrayList("methodNames");

methodData

Сопоставление каждого из methodNames с methodData .

Котлин

val methodData: Bundle? = extras.getBundle("methodData")

Java

Bundle methodData = extras.getBundle("methodData");

merchantName

Содержимое HTML-тега <title> страницы оформления заказа продавца (контекст просмотра верхнего уровня в браузере).

Котлин

val merchantName: String? = extras.getString("merchantName")

Java

String merchantName = extras.getString("merchantName");

topLevelOrigin

Источник продавца без схемы (источник контекста просмотра верхнего уровня без указания схемы). Например, https://mystore.com/checkout передается как mystore.com .

Котлин

val topLevelOrigin: String? = extras.getString("topLevelOrigin")

Java

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")
}

Java

Parcelable[] topLevelCertificateChain =
        extras.getParcelableArray("topLevelCertificateChain");
if (topLevelCertificateChain != null) {
    for (Parcelable p : topLevelCertificateChain) {
        if (p != null && p instanceof Bundle) {
            ((Bundle) p).getByteArray("certificate");
        }
    }
}

paymentRequestOrigin

Параметр `setScheme` указывает на источник контекста просмотра iframe, вызвавшего конструктор ` new PaymentRequest(methodData, details, options) в JavaScript. Если конструктор был вызван из контекста верхнего уровня, то значение этого параметра равно значению параметра ` topLevelOrigin .

Котлин

val paymentRequestOrigin: String? = extras.getString("paymentRequestOrigin")

Java

String paymentRequestOrigin = extras.getString("paymentRequestOrigin");

total

JSON-строка, представляющая общую сумму транзакции.

Котлин

val total: String? = extras.getString("total")

Java

String total = extras.getString("total");

Вот пример содержимого строки:

{"currency":"USD","value":"25.00"}

modifiers

Результат выполнения JSON.stringify(details.modifiers) , где details.modifiers содержат только supportedMethods , data и total .

paymentRequestId

Поле PaymentRequest.id , которое приложения для «принудительных платежей» должны связывать с состоянием транзакции. Веб-сайты продавцов будут использовать это поле для запроса состояния транзакции у приложений для «принудительных платежей» внеполосным способом.

Котлин

val paymentRequestId: String? = extras.getString("paymentRequestId")

Java

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()

Java

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 . В этом случае request.show() будет отклонен с AbortError на веб-сайте продавца, указывающей на отмену транзакции пользователем.

Котлин

setResult(Activity.RESULT_CANCELED)
finish()

Java

setResult(Activity.RESULT_CANCELED);
finish();

Начиная с версии Chrome 149, поддерживаются следующие значения результатов:

Котлин

Activity.RESULT_CANCELED // 0 (0x00000000)
Activity.RESULT_OK // -1 (0xffffffff)
const val INTERNAL_PAYMENT_APP_ERROR = Activity.RESULT_FIRST_USER // 1 (0x00000001)

Java

Activity.RESULT_CANCELED // 0 (0x00000000)
Activity.RESULT_OK // -1 (0xffffffff)
static final int INTERNAL_PAYMENT_APP_ERROR = Activity.RESULT_FIRST_USER; // 1 (0x00000001)

Если приложение для оплаты не сработало из-за внутренней ошибки, вы можете указать на это, передав Activity.RESULT_FIRST_USER в качестве кода результата.

Если возвращается значение INTERNAL_PAYMENT_APP_ERROR , request.show() отклонит запрос с ошибкой OperationError на веб-сайте продавца, указывающей на ошибку в платежном приложении.

Различие между RESULT_CANCELED (0) для отмены пользователем, вызывающей AbortError , и INTERNAL_PAYMENT_APP_ERROR (1) для внутренней ошибки приложения, вызывающей OperationError , позволяет продавцам создавать более эффективные пользовательские сценарии.

Котлин

setResult(Activity.RESULT_FIRST_USER)
finish()

Java

setResult(Activity.RESULT_FIRST_USER);
finish();

Если результат обработки платежного запроса, полученного от запущенного платежного приложения, имеет значение RESULT_OK , Chrome проверит наличие непустого значения methodName и details в своих extras. Если проверка не пройдена, Chrome вернет отклоненный промис из request.show() с одним из следующих сообщений об ошибке, с которыми могут столкнуться разработчики:

'Payment app returned invalid response. Missing field "details".'
'Payment app returned invalid response. Missing field "methodName".'

Разрешение

Данная активность может проверить вызывающий объект с помощью своего метода getCallingPackage() .

Котлин

val caller: String? = callingPackage

Java

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
)

Java

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)

Java

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