Guida per gli sviluppatori di app per pagamenti Android

Scopri come adattare la tua app per pagamenti Android in modo che funzioni con i pagamenti web e offrire un'esperienza utente migliore ai clienti.

L'API Payment Request porta sul web un'interfaccia integrata basata su browser che consente agli utenti di inserire le informazioni di pagamento richieste più facilmente che mai. L'API può anche richiamare app di pagamento specifiche della piattaforma.

Supporto dei browser

  • 60
  • 15
  • 11.1

Fonte

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

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

  • L'app di pagamento viene lanciata in modalità modale, nel contesto del sito web del commerciante.
  • L'implementazione è supplementare all'app per pagamenti esistente e ti consente di sfruttare la tua base utenti.
  • La firma dell'app di pagamento viene controllata per evitare il sideload.
  • Le app di pagamento possono supportare più metodi di pagamento.
  • Può essere integrato qualsiasi metodo di pagamento, come criptovaluta, bonifici bancari e altro ancora. Le app di pagamento sui dispositivi Android possono anche integrare metodi che richiedono l'accesso al chip hardware sul dispositivo.

Per implementare Web Payments in un'app per pagamenti Android sono previsti quattro passaggi:

  1. Consenti ai commercianti di trovare 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 un pagamento.
  4. Verifica il certificato di firma del chiamante.

Per vedere come funziona Web Payments, guarda la demo di android-web-payment.

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

Per poter utilizzare la tua app di pagamento, un commerciante deve usare l'API Payment Request 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 file 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 per 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 nell'app di pagamento non è presente un gestore di intent di questo tipo, il browser web presuppone 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:

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

Implementare IsReadyToPayService

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

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

Risposta

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

callback?.handleIsReadyToPay(true)

Autorizzazione

Puoi utilizzare Binder.getCallingUid() per verificare chi è il chiamante. Tieni presente che devi eseguire questa operazione nel metodo isReadyToPay, non nel metodo onBind.

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

Vedi Verificare il certificato di firma del chiamante per informazioni su come verificare che il pacchetto per le chiamate abbia la firma corretta.

Passaggio 3: consenti a un cliente di effettuare il pagamento

Il commerciante chiama show() per lanciare l'app di pagamento in modo che il cliente possa effettuare un pagamento. L'app di pagamento viene richiamata tramite un intent Android PAY con informazioni sulle transazioni nei parametri dell'intent.

L'app per pagamenti risponde con methodName e details, che sono specifiche dell'app per pagamenti e sono opache per il browser. Il browser converte la stringa details in un oggetto JavaScript per il commerciante tramite la deserializzazione JSON, ma non impone alcuna validità oltre questo. Il browser non modifica l'details; il valore di questo parametro viene trasmesso direttamente al commerciante.

AndroidManifest.xml

L'attività con il filtro per 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/method_names" />
</activity>

resource deve essere un elenco di stringhe, ognuna delle quali deve essere un URL valido e assoluto con uno schema HTTPS, come mostrato qui.

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

Parametri

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

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

methodNames

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

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

methodData

Una mappatura da ogni elemento methodNames a methodData.

val methodData: Bundle? = 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).

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

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

topLevelCertificateChain

La catena di certificati del commerciante (la catena di certificati del contesto di navigazione di primo livello). Null per localhost e file su disco, che sono entrambi contesti sicuri senza certificati SSL. Ogni Parcelable è un bundle con una chiave certificate e un valore array di byte.

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

paymentRequestOrigin

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

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

total

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

val total: String? = 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 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 interrogare le app "push-payment" sullo stato della transazione fuori banda.

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

Risposta

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

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

Devi specificare due parametri come extra di intent:

  • methodName: il nome del metodo utilizzato.
  • details: stringa JSON contenente le informazioni necessarie al commerciante per completare la transazione. Se l'esito è true, details deve essere costruito in modo tale che JSON.parse(details) possa avere successo.

Puoi trasmettere RESULT_CANCELED se la transazione non è stata completata nell'app per i pagamenti, ad esempio se l'utente non ha digitato il codice PIN corretto del proprio account nell'app per i pagamenti. Il browser potrebbe consentire all'utente di scegliere un'app di pagamento diversa.

setResult(RESULT_CANCELED)
finish()

Se il risultato dell'attività di una risposta di pagamento ricevuta dall'app di pagamento richiamata è impostato su RESULT_OK, Chrome controlla se sono presenti methodName e details non vuoti. Se la convalida non va a buon fine, Chrome restituirà una promessa rifiutata da request.show() con uno dei seguenti messaggi di errore visualizzati dagli sviluppatori:

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

Autorizzazione

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

val caller: String? = callingPackage

Il passaggio finale consiste nel verificare il certificato di firma del chiamante per confermare che il pacchetto di chiamata abbia la firma corretta.

Passaggio 4: verifica il certificato di firma del chiamante

Puoi controllare il nome del pacchetto del chiamante 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 hai scelto come target il livello API 28 o versioni successive e stai integrando un browser con un singolo certificato di firma, puoi utilizzare 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() è preferibile per i browser a certificato singolo, perché gestisce correttamente la rotazione dei certificati. (Chrome ha un singolo certificato di firma). Le app con più certificati di firma non possono ruotare.

Se devi supportare i livelli API precedenti 27 e precedenti o se devi gestire browser con più certificati di firma, puoi utilizzare 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) } }