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.
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.
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 all'app di pagamento 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.
- È 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 sul dispositivo.
Per implementare i pagamenti web in un'app di pagamento per Android sono necessari quattro passaggi:
- Consenti ai commercianti di scoprire la tua app di pagamento.
- Comunica a un commerciante se un cliente ha uno strumento registrato (ad esempio una carta di credito) pronto per il pagamento.
- Consenti a un cliente di effettuare il pagamento.
- 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
Per poter utilizzare la tua app di pagamento, un commerciante deve utilizzare l'API Payment Request e specificare il metodo di pagamento supportato tramite l'identificatore del metodo di pagamento.
Se hai un identificatore del metodo di pagamento univoco per la tua app di pagamento, puoi configurare un file manifest del metodo di pagamento personale in modo che i browser possano scoprire 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:
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 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())
// …
Consulta la sezione Verificare il certificato di firma dell'autore della chiamata per scoprire come verificare che il pacchetto chiamante 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 invocata tramite un intentoPAY
Android con le informazioni sulla transazione nei parametri dell'intento.
L'app di pagamento risponde con methodName
e details
, valori specifici dell'app di pagamento e opachi per il browser. Il browser converte la stringa details
in un oggetto JavaScript per il commerciante tramite la deserializzazione JSON, ma
non applica alcuna validità oltre a quella. Il browser non modificadetails
; 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/method_names" />
</activity>
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="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 di 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 nel
dizionario methodData
. Questi sono i metodi supportati dall'app di pagamento.
val methodNames: List<String>? = extras.getStringArrayList("methodNames")
methodData
Una mappatura da ogni 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 eseguire una query
alle app di "pagamento push" per conoscere lo stato della transazione.
val paymentRequestId: String? = extras.getString("paymentRequestId")
Risposta
L'attività può inviare 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 per il completamento della transazione da parte del commerciante. Se il risultato ètrue
,details
deve essere costruito in modo cheJSON.parse(details)
abbia esito positivo.
Puoi trasferire RESULT_CANCELED
se la transazione non è stata completata nell'app di pagamento, ad esempio se l'utente non ha inserito il codice PIN corretto per il proprio account nell'app di pagamento. Il browser potrebbe consentire all'utente di scegliere un'altra app per i pagamenti.
setResult(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()
.
val caller: String? = callingPackage
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()
.
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 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 ruotarli.
Se hai bisogno di supportare livelli API precedenti 27 e precedenti o se hai bisogno di 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) } }