Guía para desarrolladores de aplicaciones de pago Android

Aprende a adaptar tu app de pagos para Android de modo que funcione con los Pagos web y brinde una mejor experiencia del usuario a los clientes.

La Payment Request API ofrece web una interfaz integrada basada en el navegador que permite a los usuarios ingresar el pago requerido la información sea más fácil que nunca. La API también puede invocar pagos específicos de la plataforma de Google Chat.

Navegadores compatibles

  • Chrome: 60.
  • Límite: 15.
  • Firefox: detrás de una marca.
  • Safari: 11.1.

Origen

Flujo de confirmación de la compra con la app de Google Pay específica de la plataforma que usa Pagos web.

En comparación con el uso exclusivo de intents de Android, los pagos web permiten una mejor integración. con el navegador, la seguridad y la experiencia del usuario:

  • La app de pagos se inicia como modal, en el contexto del sitio web del comercio.
  • La implementación es complementaria a tu app de pagos existente, lo que te permite hacer lo siguiente: aprovechar tu base de usuarios.
  • Se marcó la firma de la app de pago para evitar transferencia.
  • Las apps de pagos admiten varias formas de pago.
  • Cualquier forma de pago, como criptomonedas, transferencias bancarias y más, integrado. En los dispositivos Android, las apps de pago incluso pueden integrar métodos que requieren acceso al chip de hardware del dispositivo.

Para implementar los pagos web en una app de pagos para Android, debes seguir cuatro pasos:

  1. Permite que los comercios descubran tu app de pagos.
  2. Informa a un comercio si un cliente tiene un instrumento inscrito (como crédito tarjeta) que está listo para pagar.
  3. Permite que un cliente realice un pago.
  4. Verifica el certificado de firma del emisor.

Para ver los Pagos web en acción, consulta la android-web-payment demostración.

Paso 1: Permite que los comercios descubran tu app de pagos

Para que un comerciante pueda utilizar tu aplicación de pagos, debe utilizar la página Pagos API de solicitud y Especifica la forma de pago que admites con la forma de pago identificador.

Si tienes un identificador de forma de pago único para tu app de pagos, puedes puedes configurar tu propia forma de pago de Terraform para que los navegadores puedan descubrir tu app.

Paso 2: Informa al comercio si un cliente tiene un instrumento inscrito que esté listo para pagar

El comercio puede llamar a hasEnrolledInstrument() para consultar si el cliente puede realizar un pago. Puedes Implementa IS_READY_TO_PAY como servicio de Android para responder esta consulta.

AndroidManifest.xml

Declara tu servicio con un filtro de intents con la acción 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>

El servicio IS_READY_TO_PAY es opcional. Si no existe tal controlador de intents en pago, el navegador web da por sentado que la app siempre puede realizar pagos.

AIDL

La API para el servicio IS_READY_TO_PAY se define en AIDL. Crea dos AIDL con el siguiente contenido:

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

Cómo implementar IsReadyToPayService

La implementación más simple de IsReadyToPayService se muestra a continuación ejemplo:

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

Respuesta

El servicio puede enviar su respuesta a través del método handleIsReadyToPay(Boolean).

callback?.handleIsReadyToPay(true)

Permiso

Puedes usar Binder.getCallingUid() para verificar quién es el emisor. Ten en cuenta que debes hacerlo en el método isReadyToPay, no en el método onBind.

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

Consulta Cómo verificar el certificado de firma del emisor para obtener información sobre cómo hacerlo. para verificar que el paquete de llamada tenga la firma correcta.

Paso 3: Permite que un cliente realice un pago

El comerciante llama a show() para iniciar el pago. app para que el cliente pueda realizar un pago. La app de pago se invoca mediante un código El intent PAY con información de transacción en los parámetros del intent

La app de pagos responde con methodName y details, que son apps de pagos. específicas y son opacas para el navegador. El navegador convierte el details en un objeto de JavaScript para el comerciante a través de la deserialización JSON, pero no aplica ninguna validez más allá de eso. El navegador no modifica la details; el valor de ese parámetro se pasa directamente al comercio.

AndroidManifest.xml

La actividad con el filtro de intents PAY debe tener una etiqueta <meta-data> que identifica el identificador de la forma de pago predeterminada para la en tu app.

Para admitir varias formas de pago, agrega una etiqueta <meta-data> con una Recurso <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 debe ser una lista de cadenas, cada una de las cuales debe ser un valor válido, absoluta de Google con un esquema HTTPS, como se muestra aquí.

<?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>

Parámetros

Los siguientes parámetros se pasan a la actividad como extras de intent:

  • methodNames
  • methodData
  • topLevelOrigin
  • topLevelCertificateChain
  • paymentRequestOrigin
  • total
  • modifiers
  • paymentRequestId
val extras: Bundle? = intent?.extras

methodNames

Los nombres de los métodos que se usan. Los elementos son las claves en las Diccionario de methodData. Estas son las formas que admite la app de pagos.

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

methodData

Una asignación de cada uno de los methodNames al methodData

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

merchantName

El contenido de la etiqueta HTML <title> de la página de confirmación de compras del comercio (la contexto de navegación de nivel superior del navegador).

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

topLevelOrigin

El origen del comerciante sin el esquema (el origen sin esquema del contexto de navegación de nivel superior). Por ejemplo, https://mystore.com/checkout es pasado como mystore.com.

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

topLevelCertificateChain

La cadena de certificados del comercio (la cadena de certificados de la contexto de navegación). Nulo para localhost y archivo en el disco, que son seguros. sin certificados SSL. Cada Parcelable es un paquete con un Clave certificate y valor de array de bytes.

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

paymentRequestOrigin

El origen sin esquema del contexto de navegación de iframe que invocó el constructor new PaymentRequest(methodData, details, options) en JavaScript. Si el botón se invocó desde el contexto de nivel superior, el valor de este es igual al valor del parámetro topLevelOrigin.

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

total

Es la cadena JSON que representa el importe total de la transacción.

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

Este es un ejemplo de contenido de la cadena:

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

modifiers

El resultado de JSON.stringify(details.modifiers), en el que details.modifiers contienen solo supportedMethods y total.

paymentRequestId

El campo PaymentRequest.id que indica "push-payment" las apps deben asociarse con el estado de transacción. Los sitios web de comercios usarán este campo para consultar &quot;push-payment&quot; apps para el estado de transacción fuera de banda.

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

Respuesta

La actividad puede enviar su respuesta a través de setResult con RESULT_OK.

setResult(Activity.RESULT_OK, Intent().apply {
  putExtra("methodName", "https://bobbucks.dev/pay")
  putExtra("details", "{\"token\": \"put-some-data-here\"}")
})
finish()

Debes especificar dos parámetros como extras de intent:

  • methodName: Es el nombre del método que se usa.
  • details: Es una cadena JSON que contiene la información necesaria para que el comercio pueda hacer lo siguiente: completar la transacción. Si el valor es true, entonces details debe construido de tal manera que JSON.parse(details) funcionará correctamente.

Puedes pasar RESULT_CANCELED si la transacción no se completó en el app de pago, por ejemplo, si el usuario no escribe el código PIN correcto de su cuenta en la aplicación de pago. El navegador puede permitirle al usuario elegir otra app de pagos.

setResult(RESULT_CANCELED)
finish()

Si el resultado de la actividad de una respuesta de pago recibida del pago invocado la app está configurada como RESULT_OK, Chrome buscará methodName que no estén vacías details en sus extras. Si la validación falla, Chrome devolverá promesa de request.show() con uno de los siguientes errores para el desarrollador mensajes:

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

Permiso

La actividad puede verificar el llamador con su método getCallingPackage().

val caller: String? = callingPackage

El último paso es verificar el certificado de firma del emisor para confirmar que el que llama tiene la firma correcta.

Paso 4: Verifica el certificado de firma del emisor

Puedes verificar el nombre del paquete del llamador con Binder.getCallingUid() en IS_READY_TO_PAY y Activity.getCallingPackage() en PAY. Para verificar que el emisor sea el navegador que tienes en mente, deberías revisa el certificado de firma y asegúrate de que coincida con el valor.

Si tu objetivo es el nivel de API 28 o uno superior, y realizas la integración con un navegador que tiene un único certificado de firma, puedes usar 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
)

Se prefiere PackageManager.hasSigningCertificate() para un solo certificado. navegadores, ya que controla correctamente la rotación de certificados. (Chrome tiene un certificado de firma único). Las apps que tienen varios certificados de firma no pueden rotarlas.

Si necesitas admitir niveles de API anteriores 27 o inferiores, o si necesitas administrar con múltiples certificados de firma, puedes usar 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) } }