Guida per gli sviluppatori di app per pagamenti Android

Scopri come adattare la tua app di pagamento per Android in modo che funzioni con i pagamenti web e offri una migliore esperienza utente ai clienti.

Pubblicato: 5 maggio 2020, ultimo aggiornamento: 27 maggio 2025

L'API Payment Request offre al web un'interfaccia basata su browser integrata che consente agli utenti di inserire i dati di pagamento richiesti più facilmente che mai. L'API può anche richiamare app di pagamento specifiche della piattaforma.

Browser Support

  • Chrome: 60.
  • Edge: 15.
  • Firefox: behind a flag.
  • Safari: 11.1.

Source

Flusso di pagamento con l'app Google Pay specifica per la piattaforma che utilizza i pagamenti web.

Rispetto all'utilizzo solo di intent Android, i pagamenti web consentono una migliore integrazione con il browser, la sicurezza e l'esperienza utente:

  • L'app di pagamento viene lanciata come finestra modale nel contesto del sito web del commerciante.
  • L'implementazione è supplementare alla tua app di pagamento esistente, consentendoti di sfruttare la tua base utenti.
  • La firma dell'app di pagamento viene controllata per impedire il sideload.
  • Le app di pagamento possono supportare più metodi di pagamento.
  • È possibile integrare qualsiasi metodo di pagamento, ad esempio criptovalute, bonifici bancari e altro ancora. Le app di pagamento sui dispositivi Android possono persino integrare metodi che richiedono l'accesso al chip hardware del dispositivo.

Per implementare i pagamenti web in un'app di pagamento per Android sono necessari quattro passaggi:

  1. Consenti ai commercianti di scoprire la tua app di pagamento.
  2. Comunica a un commerciante se un cliente ha uno strumento registrato (ad esempio una carta di credito) pronto per il pagamento.
  3. Consenti a un cliente di effettuare il pagamento.
  4. Verifica il certificato di firma del chiamante.

Per vedere come funzionano i pagamenti web, dai un'occhiata alla demo di android-web-payment.

Passaggio 1: consenti ai commercianti di trovare la tua app di pagamento

Imposta la proprietà related_applications nel file manifest dell'app web seguendo le istruzioni riportate in Configurare un metodo di pagamento.

Affinché un commerciante possa utilizzare la tua app di pagamento, deve utilizzare l'API PaymentRequest e specificare il metodo di pagamento supportato utilizzando l'identificatore del metodo di pagamento.

Se hai un identificatore del metodo di pagamento univoco per la tua app di pagamento, puoi configurare il tuo manifest del metodo di pagamento in modo che i browser possano trovare la tua app.

Passaggio 2: comunica a un commerciante se un cliente ha uno strumento registrato pronto per il pagamento

Il commerciante può chiamare hasEnrolledInstrument() per chiedere se il cliente è in grado di effettuare un pagamento. Puoi implementare IS_READY_TO_PAY come servizio Android per rispondere a questa query.

AndroidManifest.xml

Dichiara il tuo servizio con un filtro intent con l'azione 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>

Il servizio IS_READY_TO_PAY è facoltativo. Se non è presente un gestore di intent di questo tipo nell'app di pagamento, il browser web presume che l'app possa sempre effettuare pagamenti.

AIDL

L'API per il servizio IS_READY_TO_PAY è definita in AIDL. Crea due file AIDL con i seguenti contenuti:

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

Implementare IsReadyToPayService

L'implementazione più semplice di IsReadyToPayService è mostrata nell'esempio seguente:

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

Risposta

Il servizio può inviare la risposta utilizzando il metodo handleIsReadyToPay(Boolean).

KotlinJava
callback?.handleIsReadyToPay(true)
if (callback != null) {
    callback.handleIsReadyToPay(true);
}

Autorizzazione

Puoi utilizzare Binder.getCallingUid() per controllare chi è chi chiama. Tieni presente che devi farlo nel metodo isReadyToPay, non nel metodo onBind, perché il sistema operativo Android può memorizzare nella cache e riutilizzare la connessione al servizio, il che non attiva il metodo onBind().

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

Controlla sempre i parametri di input per null quando ricevi chiamate di comunicazione interprocesso (IPC). Questo è particolarmente importante perché versioni o forking diversi del sistema operativo Android possono comportarsi in modi imprevisti e causare errori se non gestiti.

Sebbene packageManager.getPackagesForUid() solitamente reti un singolo elemento, il codice deve gestire lo scenario inusuale in cui un chiamante utilizza più nomi di package. In questo modo, la tua applicazione rimane robusta.

Consulta la sezione Verificare il certificato di firma dell'autore della chiamata per scoprire come verificare che il pacchetto di chiamata abbia la firma corretta.

Parametri

Il bundle parameters è stato aggiunto in Chrome 139. Deve essere sempre controllato in base a null.

I seguenti parametri vengono trasmessi al servizio nel bundle parameters:

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

packageName è stato aggiunto in Chrome 138. Devi verificare questo parametro in base a Binder.getCallingUid() prima di utilizzare il relativo valore. Questa verifica è essenziale perché il bundle parameters è sotto il controllo completo dell'utente che effettua la chiamata, mentre Binder.getCallingUid() è controllato dal sistema operativo Android.

topLevelCertificateChain è null in WebView e sui siti web non https tipicamente utilizzati per i test locali, come http://localhost.

Passaggio 3: consentire a un cliente di effettuare il pagamento

Il commerciante chiama show() per avviare l'app di pagamento in modo che il cliente possa effettuare un pagamento. L'app di pagamento viene invocata utilizzando un'intent Android PAY con le informazioni sulla transazione nei parametri dell'intent.

L'app di pagamento risponde con methodName e details, che sono specifici dell'app di pagamento e sono opachi per il browser. Il browser converte la stringa details in un dizionario JavaScript per il commerciante utilizzando la deserializzazione della stringa JSON, ma non applica alcuna validità oltre a quella. Il browser non modifica details; il valore di questo parametro viene passato direttamente al commerciante.

AndroidManifest.xml

L'attività con il filtro intent PAY deve avere un tag <meta-data> che identifica l'identificatore del metodo di pagamento predefinito per l' app.

Per supportare più metodi di pagamento, aggiungi un tag <meta-data> con una risorsa <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 deve essere un elenco di stringhe, ognuna delle quali deve essere un URL assoluto valido con uno schema HTTPS, come mostrato di seguito.

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

Parametri

I seguenti parametri vengono passati all'attività come extra Intent:

  • methodNames
  • methodData
  • merchantName
  • topLevelOrigin
  • topLevelCertificateChain
  • paymentRequestOrigin
  • total
  • modifiers
  • paymentRequestId
  • paymentOptions
  • shippingOptions

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

methodNames

I nomi dei metodi utilizzati. Gli elementi sono le chiavi nel dizionario methodData. Questi sono i metodi supportati dall'app di pagamento.

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

methodData

Una mappatura da ogni methodNames a methodData.

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

merchantName

I contenuti del tag HTML <title> della pagina di pagamento del commerciante (il contesto di navigazione di primo livello del browser).

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

topLevelOrigin

L'origine del commerciante senza lo schema (l'origine senza schema del contesto di navigazione di primo livello). Ad esempio, https://mystore.com/checkout viene passato come mystore.com.

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

topLevelCertificateChain

La catena di certificati del commerciante (la catena di certificati del contesto di navigazione di primo livello). Il valore è null per WebView, localhost o un file su disco. Ogni Parcelable è un Bundle con una chiave certificate e un valore array di byte.

KotlinJava
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

L'origine senza schema del contesto di navigazione dell'iframe che ha invocato il costruttore new PaymentRequest(methodData, details, options) in JavaScript. Se il costruttore è stato invocato dal contesto di primo livello, il valore di questo parametro è uguale al valore del parametro topLevelOrigin.

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

total

La stringa JSON che rappresenta l'importo totale della transazione.

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

Ecco un esempio di contenuto della stringa:

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

modifiers

L'output di JSON.stringify(details.modifiers), dove details.modifiers contiene solo supportedMethods, data e total.

paymentRequestId

Il campo PaymentRequest.id che le app di "pagamento push" devono associare allo stato della transazione. I siti web dei commercianti utilizzeranno questo campo per eseguire query sulle app di "pagamento push" per lo stato della transazione out of band.

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

Risposta

L'attività può inviare la risposta tramite setResult con RESULT_OK.

KotlinJava
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("instrumentDetails", "{\"token\": \"put-some-data-here\"}");
result.putExtras(extras);
setResult(Activity.RESULT_OK, result);
finish();

Devi specificare due parametri come extra di Intent:

  • methodName: il nome del metodo utilizzato.
  • details: stringa JSON contenente le informazioni necessarie per il completamento della transazione da parte del commerciante. Se il risultato è true, details deve essere costruito in modo che JSON.parse(details) abbia esito positivo. Se non è necessario restituire alcun dato, questa stringa può essere "{}", che il sito web del commerciante riceverà come dizionario JavaScript vuoto.

Puoi passare RESULT_CANCELED se la transazione non è stata completata nell'app di pagamento, ad esempio se l'utente non è riuscito a digitare il codice PIN corretto per il suo account nell'app di pagamento. Il browser potrebbe consentire all'utente di scegliere un'app di pagamento diversa.

KotlinJava
setResult(Activity.RESULT_CANCELED)
finish()
setResult(Activity.RESULT_CANCELED);
finish();

Se il risultato dell'attività di una risposta di pagamento ricevuta dall'app di pagamento invocata è impostato su RESULT_OK, Chrome controllerà la presenza di methodName e details non vuoti nei suoi extra. Se la convalida non va a buon fine, Chrome restituirà una promessa rifiutata da request.show() con uno dei seguenti messaggi di errore rivolti agli sviluppatori:

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

Autorizzazione

L'attività può controllare chi chiama con il metodo getCallingPackage().

KotlinJava
val caller: String? = callingPackage
String caller = getCallingPackage();

Il passaggio finale consiste nel verificare il certificato di firma dell'utente che chiama per confermare che il package di chiamata abbia la firma corretta.

Passaggio 4: verifica il certificato di firma dell'utente che chiama

Puoi controllare il nome del pacchetto dell'autore della chiamata con Binder.getCallingUid() in IS_READY_TO_PAY e con Activity.getCallingPackage() in PAY. Per verificare effettivamente che il chiamante sia il browser che hai in mente, devi controllare il relativo certificato di firma e assicurarti che corrisponda al valore corretto.

Se scegli come target il livello API 28 e versioni successive e esegui l'integrazione con un browser con un singolo certificato di firma, puoi utilizzare PackageManager.hasSigningCertificate().

KotlinJava
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() è preferibile per i browser con un solo certificato, in quanto gestisce correttamente la rotazione dei certificati. Chrome ha un singolo certificato di firma. Le app con più certificati di firma non possono girarli.

Se devi supportare i livelli API 27 e precedenti o se devi gestire browser con più certificati di firma, puoi utilizzare PackageManager.GET_SIGNATURES.

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

Debug

Utilizza il seguente comando per osservare errori o messaggi informativi:

adb logcat | grep -i pay