Entwicklerleitfaden für Android-Zahlungs-Apps

Hier erfahren Sie, wie Sie Ihre Android-Zahlungs-App für Web Payments anpassen und so Ihren Kunden eine bessere Nutzererfahrung bieten können.

Die Payment Request API bietet eine integrierte browserbasierte Oberfläche im Web, mit der Nutzer die erforderlichen Zahlungsinformationen einfacher als je zuvor eingeben können. Die API kann auch plattformspezifische Zahlungs-Apps aufrufen.

Unterstützte Browser

  • 60
  • 15
  • 11.1

Quelle

Bezahlvorgang mit einer plattformspezifischen Google Pay-App, die Webzahlungen verwendet.

Im Vergleich zur Verwendung reiner Android-Intents ermöglichen Web Payments eine bessere Integration mit dem Browser, der Sicherheit und der Nutzererfahrung:

  • Die Zahlungs-App wird als modales Fenster im Kontext der Händlerwebsite gestartet.
  • Die Implementierung ist eine Ergänzung zu deiner bestehenden Zahlungs-App, sodass du deine Nutzerbasis optimal nutzen kannst.
  • Die Signatur der Zahlungs-App wird geprüft, um ein Sideloading zu verhindern.
  • Zahlungs-Apps unterstützen mehrere Zahlungsmethoden.
  • Alle Zahlungsmethoden, z. B. Kryptowährungen und Banküberweisungen, können integriert werden. Zahlungs-Apps auf Android-Geräten können sogar Methoden integrieren, die Zugriff auf den Hardwarechip des Geräts erfordern.

Zur Implementierung von Web Payments in einer Android-Zahlungs-App sind vier Schritte erforderlich:

  1. Machen Sie Händler auf Ihre Zahlungs-App aufmerksam.
  2. Teile einem Händler mit, ob ein Kunde ein registriertes Zahlungsmittel (z. B. eine Kreditkarte) hat, mit dem du bezahlen kannst.
  3. Kunden die Zahlung ermöglichen lassen.
  4. Prüfen Sie das Signaturzertifikat des Aufrufers.

Sieh dir die Demo zu android-web-payment an, um Web Payments in Aktion zu sehen.

Schritt 1: Händler Ihre Zahlungs-App finden lassen

Damit ein Händler deine Zahlungs-App nutzen kann, muss er die Payment Request API verwenden und die von dir unterstützte Zahlungsmethode mithilfe der Zahlungsmethode angeben.

Wenn du eine eindeutige Zahlungsmethoden-ID für deine Zahlungs-App hast, kannst du ein eigenes Manifest für die Zahlungsmethode einrichten, damit Browser deine App finden können.

Schritt 2: Händler informieren, ob ein Kunde ein registriertes Zahlungsmittel hat, das zum Bezahlen bereitsteht

Der Händler kann hasEnrolledInstrument() aufrufen, um abzufragen, ob der Kunde eine Zahlung ausführen kann. Sie können IS_READY_TO_PAY als Android-Dienst implementieren, um diese Abfrage zu beantworten.

AndroidManifest.xml

Deklarieren Sie Ihren Dienst mit einem Intent-Filter mit der Aktion 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>

Der Dienst IS_READY_TO_PAY ist optional. Wenn kein solcher Intent-Handler in der Zahlungs-App vorhanden ist, geht der Webbrowser davon aus, dass die App immer Zahlungen vornehmen kann.

AIDL

Die API für den Dienst IS_READY_TO_PAY ist in AIDL definiert. Erstellen Sie zwei AIDL-Dateien mit folgendem Inhalt:

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

IsReadyToPayService implementieren

Das folgende Beispiel zeigt die einfachste Implementierung von IsReadyToPayService:

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

Antwort

Der Dienst kann seine Antwort über die Methode handleIsReadyToPay(Boolean) senden.

callback?.handleIsReadyToPay(true)

Berechtigung

Mit Binder.getCallingUid() können Sie prüfen, wer der Aufrufer ist. Dies müssen Sie in der Methode isReadyToPay und nicht in der Methode onBind tun.

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

Wie du prüfen kannst, ob das aufrufende Paket die richtige Signatur hat, erfährst du unter Signaturzertifikat des Aufrufers prüfen.

Schritt 3: Kunden die Zahlung vornehmen lassen

Der Händler ruft show() auf, um die Zahlungs-App zu starten, damit der Kunde eine Zahlung vornehmen kann. Die Zahlungs-App wird über einen Android-Intent-PAY mit Transaktionsinformationen in den Intent-Parametern aufgerufen.

Die Zahlungs-App antwortet mit methodName und details. Diese sind spezifisch für die Zahlungs-App und für den Browser undurchsichtig. Der Browser konvertiert den String details in ein JavaScript-Objekt für den Händler über die JSON-Deserialisierung, erzwingt jedoch keine darüber hinausgehende Gültigkeit. Der Browser nimmt keine Änderungen am details vor. Der Wert dieses Parameters wird direkt an den Händler übergeben.

AndroidManifest.xml

Die Aktivität mit dem Intent-Filter PAY sollte ein <meta-data>-Tag haben, das die standardmäßige Kennung der Zahlungsmethode für die App angibt.

Wenn du mehrere Zahlungsmethoden unterstützen möchtest, füge ein <meta-data>-Tag mit einer <string-array>-Ressource hinzu.

<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 muss eine Liste von Strings sein, von denen jeder eine gültige, absolute URL mit einem HTTPS-Schema sein muss, wie hier gezeigt.

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

Parameter

Die folgenden Parameter werden als Intent-Extras an die Aktivität übergeben:

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

methodNames

Die Namen der verwendeten Methoden. Die Elemente sind die Schlüssel im Wörterbuch methodData. Die folgenden Methoden werden von der Zahlungs-App unterstützt.

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

methodData

Eine Zuordnung von methodNames zum methodData.

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

merchantName

Der Inhalt des HTML-Tags <title> auf der Zahlungsseite des Händlers (der Browserkontext auf oberster Ebene des Browsers).

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

topLevelOrigin

Der Ursprung des Händlers ohne das Schema (der schemalose Ursprung des Browserkontexts der obersten Ebene). Beispiel: https://mystore.com/checkout wird als mystore.com übergeben.

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

topLevelCertificateChain

Die Zertifikatskette des Händlers (die Zertifikatskette des Browserkontexts der obersten Ebene). Null für localhost und Datei auf dem Laufwerk, die beide sichere Kontexte ohne SSL-Zertifikate sind. Jede Parcelable ist ein Bundle mit einem certificate-Schlüssel und einem Byte-Arraywert.

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

paymentRequestOrigin

Der schemalose Ursprung des iFrame-Browserkontexts, der den new PaymentRequest(methodData, details, options)-Konstruktor in JavaScript aufgerufen hat. Wenn der Konstruktor über den Kontext der obersten Ebene aufgerufen wurde, entspricht der Wert dieses Parameters dem Wert des Parameters topLevelOrigin.

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

total

Der JSON-String, der den Gesamtbetrag der Transaktion darstellt.

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

Hier ist ein Beispielinhalt des Strings:

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

modifiers

Die Ausgabe von JSON.stringify(details.modifiers), wobei details.modifiers nur supportedMethods und total enthält.

paymentRequestId

Das Feld PaymentRequest.id, das Apps für Push-Zahlungen mit dem Transaktionsstatus verknüpfen sollen. Händlerwebsites verwenden dieses Feld, um die Push-Bezahl-Apps bezüglich des Transaktionsstatus außerhalb des Bandes abzufragen.

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

Antwort

Die Aktivität kann ihre Antwort über setResult mit RESULT_OK zurücksenden.

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

Sie müssen zwei Parameter als Intent-Extras angeben:

  • methodName: Der Name der verwendeten Methode.
  • details: JSON-String mit Informationen, die der Händler benötigt, um die Transaktion abzuschließen. Wenn der Erfolg true lautet, muss details so konstruiert sein, dass JSON.parse(details) erfolgreich ist.

Du kannst RESULT_CANCELED übergeben, wenn die Transaktion nicht in der Zahlungs-App abgeschlossen wurde, z. B. wenn der Nutzer nicht den richtigen PIN-Code für sein Konto in der Zahlungs-App eingegeben hat. Der Browser lässt den Nutzer möglicherweise eine andere Zahlungs-App auswählen.

setResult(RESULT_CANCELED)
finish()

Wenn das Aktivitätsergebnis einer Zahlungsantwort, die von der aufgerufenen Zahlungs-App empfangen wurde, auf RESULT_OK gesetzt ist, sucht Chrome in seinen Extras nach nicht leeren methodName und details. Wenn die Validierung fehlschlägt, gibt Chrome ein abgelehntes Versprechen von request.show() mit einer der folgenden für Entwickler sichtbaren Fehlermeldungen zurück:

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

Berechtigung

Die Aktivität kann den Aufrufer mit der getCallingPackage()-Methode prüfen.

val caller: String? = callingPackage

Der letzte Schritt besteht darin, das Signaturzertifikat des Aufrufers zu prüfen, um zu bestätigen, dass das aufrufende Paket die richtige Signatur hat.

Schritt 4: Signaturzertifikat des Aufrufers prüfen

Du kannst den Paketnamen des Anrufers mit Binder.getCallingUid() in IS_READY_TO_PAY und mit Activity.getCallingPackage() in PAY prüfen. Um zu überprüfen, ob es sich bei dem Aufrufer um den von Ihnen gewünschten Browser handelt, sollten Sie das Signaturzertifikat prüfen und darauf achten, dass es mit dem richtigen Wert übereinstimmt.

Wenn Ihr Ziel API-Level 28 oder höher ist und die Einbindung in einen Browser mit einem einzelnen Signaturzertifikat erfolgt, können Sie PackageManager.hasSigningCertificate() verwenden.

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() wird für Browser mit einem einzelnen Zertifikat bevorzugt, da damit die Zertifikatsrotation korrekt verarbeitet wird. (Chrome hat ein einzelnes Signaturzertifikat.) Anwendungen mit mehreren Signaturzertifikaten können diese nicht rotieren.

Wenn Sie ältere API-Level 27 und niedriger unterstützen oder Browser mit mehreren Signaturzertifikaten verwenden müssen, können Sie PackageManager.GET_SIGNATURES verwenden.

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