Descubre cómo adaptar tu app de pagos para Android para que funcione con Web Payments y brindar una mejor experiencia del usuario a los clientes.
Fecha de publicación: 5 de mayo de 2020, última actualización: 27 de mayo de 2025
La API de Payment Request incorpora a la Web una interfaz integrada basada en el navegador que permite a los usuarios ingresar la información de pago requerida más fácilmente que nunca. La API también puede invocar apps de pago específicas de la plataforma.
En comparación con el uso de intents de Android, Web Payments permite una mejor integración con el navegador, la seguridad y la experiencia del usuario:
- La app de pago se inicia como un modal, en el contexto del sitio web del comercio.
- La implementación es complementaria a tu app de pago existente, lo que te permite aprovechar tu base de usuarios.
- Se verifica la firma de la app de pago para evitar la carga lateral .
- Las apps de pago pueden admitir varias formas de pago.
- Se puede integrar cualquier forma de pago, como criptomonedas, transferencias bancarias y mucho más. Las apps de pago en dispositivos Android pueden incluso integrar métodos que requieren acceso al chip de hardware del dispositivo.
Se requieren cuatro pasos para implementar Web Payments en una app de pago para Android:
- Permite que los comercios descubran tu app de pago.
- Informa a un comercio si un cliente tiene un instrumento inscrito (como una tarjeta de crédito) que está listo para pagar.
- Permite que un cliente realice el pago.
- Verifica el certificado de firma del llamador.
Para ver Web Payments en acción, consulta la demostración de android-web-payment.
Paso 1: Permite que los comercios descubran tu app de pago
Configura la propiedad related_applications en el manifiesto de la app web según las
instrucciones que se indican en Cómo configurar una forma de pago.
Para que un comercio use tu app de pago, debe usar la API de Payment Request y especificar la forma de pago que admites con el identificador de forma de pago.
Si tienes un identificador de forma de pago que es único para tu app de pago, puedes configurar tu propio manifiesto de forma de pago para que los navegadores puedan descubrir tu app.
Paso 2: Informa a un 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 implementar IS_READY_TO_PAY como un servicio de Android para responder a 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 hay un controlador de intents de este tipo en la app de pago, el navegador web supone que la app siempre puede realizar pagos.
AIDL
La API para el servicio IS_READY_TO_PAY se define en AIDL. Crea dos archivos AIDL con el siguiente contenido:
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);
}
Cómo implementar IsReadyToPayService
La implementación más simple de IsReadyToPayService se muestra en el siguiente ejemplo:
Kotlin
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;
}
}
Respuesta
El servicio puede enviar su respuesta con el método handleIsReadyToPay(Boolean).
Kotlin
callback?.handleIsReadyToPay(true)
Java
if (callback != null) {
callback.handleIsReadyToPay(true);
}
Permiso
Puedes usar Binder.getCallingUid() para verificar quién es el llamador. Ten en cuenta que debes hacerlo en el método isReadyToPay, no en el método onBind , ya que el SO Android puede almacenar en caché y reutilizar la conexión de servicio, lo que no activa el método onBind().
Kotlin
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());
// ...
Siempre verifica los parámetros de entrada para null cuando recibas llamadas de comunicación entre procesos (IPC). Esto es particularmente importante porque las diferentes versiones o bifurcaciones del SO Android pueden comportarse de maneras inesperadas y generar errores si no se controlan.
Si bien packageManager.getPackagesForUid() suele mostrar un solo elemento, tu código debe controlar la situación poco común en la que un llamador utiliza varios nombres de paquete. Esto garantiza que tu aplicación siga siendo sólida.
Consulta Verifica el certificado de firma del llamador para obtener información sobre cómo verificar que el paquete de llamada tenga la firma correcta.
Parámetros
Se agregó el paquete parameters en Chrome 139. Siempre se debe verificar con null.
Los siguientes parámetros se pasan al servicio en el paquete parameters:
packageNamemethodNamesmethodDatatopLevelOriginpaymentRequestOrigintopLevelCertificateChain
Se agregó packageName en Chrome 138. Debes verificar este parámetro con Binder.getCallingUid() antes de usar su valor. Esta verificación es esencial porque el paquete parameters está bajo el control total del llamador, mientras que Binder.getCallingUid() está controlado por el SO Android.
topLevelCertificateChain es null en WebView y en sitios web que no son HTTPS que se suelen usar para pruebas locales, como http://localhost.
Paso 3: Permite que un cliente realice el pago
El comercio llama a show() para iniciar la app
de pago
para que el cliente pueda realizar un pago. Se invoca la app de pago con un intent de Android PAY con información de la transacción en los parámetros del intent.
La app de pago responde con methodName y details, que son específicos de la app de pago y opacos para el navegador. El navegador convierte la cadena details en un diccionario de JavaScript para el comercio mediante la deserialización de cadenas JSON, pero no aplica ninguna validez más allá de eso. El navegador no modifica los 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
identifique el identificador de forma de pago predeterminado para la
app.
Para admitir varias formas de pago, agrega una <meta-data> etiqueta con un
<string-array> recurso.
<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 debe ser una lista de cadenas, cada una de las cuales debe ser una URL absoluta válida con un esquema HTTPS, como se muestra aquí.
<?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>
Parámetros
Los siguientes parámetros se pasan a la actividad como extras de Intent:
methodNamesmethodDatamerchantNametopLevelOrigintopLevelCertificateChainpaymentRequestOrigintotalmodifierspaymentRequestIdpaymentOptionsshippingOptions
Kotlin
val extras: Bundle? = getIntent()?.extras
Java
Bundle extras = getIntent() != null ? getIntent().getExtras() : null;
methodNames
Los nombres de los métodos que se usan. Los elementos son las claves del diccionario methodData. Estos son los métodos que admite la app de pago.
Kotlin
val methodNames: List<String>? = extras.getStringArrayList("methodNames")
Java
List<String> methodNames = extras.getStringArrayList("methodNames");
methodData
Una asignación de cada uno de los methodNames a los
methodData.
Kotlin
val methodData: Bundle? = extras.getBundle("methodData")
Java
Bundle methodData = extras.getBundle("methodData");
merchantName
El contenido de la etiqueta HTML <title> de la página de confirmación de compra del comercio (el contexto de navegación de nivel superior del navegador).
Kotlin
val merchantName: String? = extras.getString("merchantName")
Java
String merchantName = extras.getString("merchantName");
topLevelOrigin
El origen del comercio sin el esquema (el origen sin esquema del contexto de navegación de nivel superior). Por ejemplo, https://mystore.com/checkout se
pasa como mystore.com.
Kotlin
val topLevelOrigin: String? = extras.getString("topLevelOrigin")
Java
String topLevelOrigin = extras.getString("topLevelOrigin");
topLevelCertificateChain
La cadena de certificados del comercio (la cadena de certificados del contexto de navegación de nivel superior). El valor es null para WebView, localhost o un archivo en el disco.
Cada Parcelable es un paquete con una clave certificate y un valor de array de bytes.
Kotlin
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
El origen sin esquema del contexto de navegación del iframe que invocó el constructor new
PaymentRequest(methodData, details, options) en JavaScript. Si se invocó el constructor desde el contexto de nivel superior, el valor de este parámetro es igual al valor del parámetro topLevelOrigin.
Kotlin
val paymentRequestOrigin: String? = extras.getString("paymentRequestOrigin")
Java
String paymentRequestOrigin = extras.getString("paymentRequestOrigin");
total
La cadena JSON que representa el importe total de la transacción.
Kotlin
val total: String? = extras.getString("total")
Java
String total = extras.getString("total");
Este es un ejemplo del contenido de la cadena:
{"currency":"USD","value":"25.00"}
modifiers
El resultado de JSON.stringify(details.modifiers), en el que details.modifiers
solo contiene supportedMethods, data y total.
paymentRequestId
El campo PaymentRequest.id que las apps de "pago push" deben asociar con el estado de la transacción. Los sitios web de los comercios usarán este campo para consultar las apps de "pago push" para conocer el estado de la transacción fuera de banda.
Kotlin
val paymentRequestId: String? = extras.getString("paymentRequestId")
Java
String paymentRequestId = extras.getString("paymentRequestId");
Respuesta
La actividad puede enviar su respuesta a través de setResult con RESULT_OK.
Kotlin
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();
Debes especificar dos parámetros como extras de Intent:
methodName: El nombre del método que se usa.details: Cadena JSON que contiene la información necesaria para que el comercio complete la transacción. Si el éxito estrue,detailsdebe construirse de tal manera queJSON.parse(details)tenga éxito. Si no hay datos que deban devolverse, esta cadena puede ser"{}", que el sitio web del comercio recibirá como un diccionario de JavaScript vacío.
Puedes pasar RESULT_CANCELED si el usuario cancela la transacción en la app de pago. Si lo haces, request.show() rechazará con un AbortError en el sitio web del comercio, lo que indica la cancelación del usuario.
Kotlin
setResult(Activity.RESULT_CANCELED)
finish()
Java
setResult(Activity.RESULT_CANCELED);
finish();
A partir de Chrome 149, se admiten los siguientes valores de resultado:
Kotlin
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)
Si la app de pago falla debido a un error interno, puedes indicarlo pasando Activity.RESULT_FIRST_USER como código de resultado.
Si se muestra INTERNAL_PAYMENT_APP_ERROR, request.show() rechazará con un OperationError en el sitio web del comercio, lo que indica un error en la app de pago.
Esta distinción entre RESULT_CANCELED (0) para la cancelación del usuario, que causa AbortError, y INTERNAL_PAYMENT_APP_ERROR (1) para un error interno de la app, que causa OperationError, permite a los comercios crear mejores flujos de usuarios.
Kotlin
setResult(Activity.RESULT_FIRST_USER)
finish()
Java
setResult(Activity.RESULT_FIRST_USER);
finish();
Si el resultado de la actividad de una respuesta de pago recibida de la app de pago invocada se establece en RESULT_OK, Chrome verificará que methodName y details no estén vacíos en sus extras. Si falla la validación, Chrome mostrará una promesa rechazada de request.show() con uno de los siguientes mensajes de error para desarrolladores:
'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().
Kotlin
val caller: String? = callingPackage
Java
String caller = getCallingPackage();
El último paso es verificar el certificado de firma del llamador para confirmar que el paquete de llamada tenga la firma correcta.
Paso 4: Verifica el certificado de firma del llamador
Puedes verificar el nombre del paquete del llamador con Binder.getCallingUid() en
IS_READY_TO_PAY y con Activity.getCallingPackage() en PAY. Para verificar que el llamador sea el navegador que tienes en mente, debes verificar su certificado de firma y asegurarte de que coincida con el valor correcto.
Si especificas el nivel de API 28 y versiones posteriores, y te integras con un navegador que tiene un solo certificado de firma, puedes usar PackageManager.hasSigningCertificate().
Kotlin
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);
Se prefiere PackageManager.hasSigningCertificate() para navegadores de un solo certificado, ya que controla correctamente la rotación de certificados. (Chrome tiene un solo certificado de firma). Las apps que tienen varios certificados de firma no pueden rotarlos.
Si necesitas admitir niveles de API 27 y versiones anteriores, o si necesitas controlar navegadores con varios certificados de firma, puedes usar PackageManager.GET_SIGNATURES.
Kotlin
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);
Depurar
Usa el siguiente comando para observar errores o mensajes informativos:
adb logcat | grep -i pay