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

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

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

Поддержка браузера

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

Источник

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

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

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

Для реализации веб-платежей в платежном приложении Android требуется четыре шага:

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

Чтобы увидеть веб-платежи в действии, посмотрите демо-версию android-web-paying .

Шаг 1. Позвольте продавцам узнать о вашем платежном приложении

Чтобы продавец мог использовать ваше платежное приложение, ему необходимо использовать 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 со следующим содержимым:

app/src/main/aidl/org/chromium/IsReadyToPayServiceCallback.aidl

package org.chromium;
interface IsReadyToPayServiceCallback {
    oneway void handleIsReadyToPay(boolean isReadyToPay);
}

app/src/main/aidl/org/chromium/IsReadyToPayService.aidl

package org.chromium;
import org.chromium.IsReadyToPayServiceCallback;

interface IsReadyToPayService {
    oneway void isReadyToPay(IsReadyToPayServiceCallback callback);
}

Реализация IsReadyToPayService

Простейшая реализация IsReadyToPayService показана в следующем примере:

class SampleIsReadyToPayService : Service() {
  private val binder = object : IsReadyToPayService.Stub() {
    override fun isReadyToPay(callback: IsReadyToPayServiceCallback?) {
      callback?.handleIsReadyToPay(true)
    }
  }

  override fun onBind(intent: Intent?): IBinder? {
    return binder
  }
}

Ответ

Служба может отправить свой ответ с помощью метода handleIsReadyToPay(Boolean) .

callback?.handleIsReadyToPay(true)

Разрешение

Вы можете использовать Binder.getCallingUid() чтобы проверить, кто звонит. Обратите внимание, что это необходимо сделать в методе isReadyToPay , а не в методе onBind .

override fun isReadyToPay(callback: IsReadyToPayServiceCallback?) {
  try {
    val callingPackage = packageManager.getNameForUid(Binder.getCallingUid())
    // …

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

Шаг 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/method_names" />
</activity>

resource должен представлять собой список строк, каждая из которых должна представлять собой действительный абсолютный URL-адрес со схемой HTTPS, как показано здесь.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="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
  • topLevelOrigin
  • topLevelCertificateChain
  • paymentRequestOrigin
  • total
  • modifiers
  • paymentRequestId
val extras: Bundle? = intent?.extras

Имена методов

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

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

methodData

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

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

имя продавца

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

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

topLevelOrigin

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

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

topLevelCertificateChain

Цепочка сертификатов продавца (Цепочка сертификатов контекста просмотра верхнего уровня). Значение NULL для локального хоста и файла на диске, которые являются безопасными контекстами без сертификатов SSL. Каждый Parcelable представляет собой Bundle с ключом certificate и значением массива байтов.

val topLevelCertificateChain: Array<Parcelable>? =
    extras.getParcelableArray("topLevelCertificateChain")
val list: List<ByteArray>? = topLevelCertificateChain?.mapNotNull { p ->
  (p as Bundle).getByteArray("certificate")
}

paymentRequestOrigin

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

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

total

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

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

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

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

modifiers

Вывод JSON.stringify(details.modifiers) , где details.modifiers содержит только supportedMethods и total .

paymentRequestId

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

val paymentRequestId: String? = 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:

  • methodName : имя используемого метода.
  • details : строка JSON, содержащая информацию, необходимую продавцу для завершения транзакции. Если успех равен true , то details должны быть созданы таким образом, чтобы JSON.parse(details) завершился успешно.

Вы можете передать RESULT_CANCELED , если транзакция не была завершена в платежном приложении, например, если пользователь не смог ввести правильный PIN-код для своей учетной записи в платежном приложении. Браузер может позволить пользователю выбрать другое платежное приложение.

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

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

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

PackageManager.hasSigningCertificate() предпочтителен для браузеров с одним сертификатом, поскольку он правильно обрабатывает ротацию сертификатов. (У Chrome один сертификат подписи.) Приложения, имеющие несколько сертификатов подписи, не могут их чередовать.

Если вам необходимо поддерживать более старые уровни API 27 и ниже или если вам нужно обрабатывать браузеры с несколькими сертификатами подписи, вы можете использовать PackageManager.GET_SIGNATURES .

val packageName: String =  // The caller's package name
val certificates: Set<ByteArray> =  // The correct set of signing certificates

val packageInfo = getPackageInfo(packageName, PackageManager.GET_SIGNATURES)
val sha256 = MessageDigest.getInstance("SHA-256")
val signatures = packageInfo.signatures.map { sha256.digest(it.toByteArray()) }
val verified = signatures.size == certificates.size &&
    signatures.all { s -> certificates.any { it.contentEquals(s) } }