Entwicklerleitfaden für Android-Zahlungs-Apps

Hier erfährst du, wie du deine Android-Zahlungs-App für Web Payments anpassen und so für eine bessere Nutzererfahrung sorgen kannst.

Die Payment Request API bietet eine integrierte browserbasierte Benutzeroberfläche, mit der Nutzer erforderliche Zahlungsinformationen einfacher denn je eingeben können. Die API kann auch plattformspezifische Zahlungs-Apps aufrufen.

Unterstützte Browser

  • Chrome: 60.
  • Edge: 15.
  • Firefox: hinter einer Flagge.
  • Safari: 11.1.

Quelle

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

Im Vergleich zur Verwendung von nur Android-Intents ermöglichen Webzahlungen eine bessere Integration in den Browser, eine höhere Sicherheit und eine bessere Nutzerfreundlichkeit:

  • Die Zahlungs-App wird als Modalfenster im Kontext der Händlerwebsite gestartet.
  • Die Implementierung ergänzt Ihre vorhandene Zahlungs-App und ermöglicht es Ihnen, Ihre Nutzerbasis zu nutzen.
  • Die Signatur der Zahlungs-App wird geprüft, um Sideloading zu verhindern.
  • Zahlungs-Apps können mehrere Zahlungsmethoden unterstützen.
  • Es können beliebige Zahlungsmethoden wie Kryptowährungen und Banküberweisungen eingebunden werden. Zahlungs-Apps auf Android-Geräten können sogar Methoden integrieren, für die der Zugriff auf den Hardwarechip des Geräts erforderlich ist.

Es sind vier Schritte erforderlich, um Webzahlungen in einer Android-Zahlungs-App zu implementieren:

  1. Händler auf Ihre Zahlungs-App aufmerksam machen
  2. Informiere den Händler, ob ein Kunde ein registriertes Zahlungsmittel (z. B. eine Kreditkarte) hat, das für die Zahlung bereit ist.
  3. Kunden die Zahlung ermöglichen
  4. Prüfen Sie das Signaturzertifikat des Anrufers.

In der Demo android-web-payment können Sie sich Webzahlungen in Aktion ansehen.

Schritt 1: Händlern die Möglichkeit geben, Ihre Zahlungs-App zu finden

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

Wenn Sie eine Zahlungsmethoden-ID haben, die nur für Ihre Zahlungs-App gilt, können Sie ein eigenes Manifest für Zahlungsmethoden einrichten, damit Ihre App von Browsern gefunden werden kann.

Schritt 2: Händler informieren, ob ein Kunde ein registriertes Zahlungsmittel hat, das zur Zahlung bereit ist

Der Händler kann hasEnrolledInstrument() anrufen, um nachzufragen, ob der Kunde eine Zahlung vornehmen 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 IS_READY_TO_PAY-Dienst ist optional. Wenn in der Zahlungs-App kein solcher Intent-Handler vorhanden ist, geht der Webbrowser davon aus, dass die App immer Zahlungen ausführen kann.

AIDL

Die API für den IS_READY_TO_PAY-Dienst 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

Die einfachste Implementierung von IsReadyToPayService wird im folgenden Beispiel gezeigt:

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 handleIsReadyToPay(Boolean)-Methode senden.

callback?.handleIsReadyToPay(true)

Berechtigung

Mit Binder.getCallingUid() kannst du prüfen, wer der Aufrufer ist. Beachte, dass du dies in der Methode isReadyToPay und nicht in der Methode onBind tun musst.

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

Unter Signaturzertifikat des Anrufers prüfen erfahren Sie, wie Sie prüfen, ob das anrufende Paket die richtige Signatur hat.

Schritt 3: Kunden die Zahlung ermöglichen

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 details-String über die JSON-Deserialisierung in ein JavaScript-Objekt für den Händler, erzwingt aber keine Gültigkeit darüber hinaus. 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 PAY-Intent-Filter muss ein <meta-data>-Tag enthalten, das die Standard-Zahlungsmethode für die App identifiziert.

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-Array-Wert.

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

paymentRequestOrigin

Der schemeless-Ursprung des iFrame-Browser-Kontexts, der den new PaymentRequest(methodData, details, options)-Konstruktor in JavaScript aufgerufen hat. Wenn der Konstruktor aus dem 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 ein Beispiel für den Inhalt 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-Zahlungs-Apps out of band nach dem Transaktionsstatus zu fragen.

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 „success“ true ist, muss details so aufgebaut sein, dass JSON.parse(details) erfolgreich ist.

Du kannst RESULT_CANCELED übergeben, wenn die Transaktion in der Zahlungs-App nicht abgeschlossen wurde, z. B. wenn der Nutzer in der Zahlungs-App nicht die richtige PIN für sein Konto eingegeben hat. Der Browser kann den Nutzer auffordern, eine andere Zahlungs-App auszuwählen.

setResult(RESULT_CANCELED)
finish()

Wenn das Aktivitätsergebnis einer Zahlungsantwort, die von der aufgerufenen Zahlungs-App empfangen wurde, auf RESULT_OK festgelegt ist, prüft Chrome in den Extras, ob methodName und details nicht leer sind. 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 Anrufer mit der Methode getCallingPackage() prüfen.

val caller: String? = callingPackage

Im letzten Schritt wird das Signaturzertifikat des Anrufers überprüft, um sicherzustellen, dass das Anrufpaket die richtige Signatur hat.

Schritt 4: Signaturzertifikat des Anrufers 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 der Anrufer der gewünschte Browser ist, sollten Sie sein Signaturzertifikat prüfen und darauf achten, dass es mit dem richtigen Wert übereinstimmt.

Wenn Sie API-Level 28 oder höher anstreben und die Integration in einen Browser mit einem einzelnen Signaturzertifikat vornehmen, 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 die Zertifikatsrotation dort korrekt verarbeitet wird. Chrome hat ein einzelnes Signaturzertifikat. Anwendungen mit mehreren Signaturzertifikaten können diese nicht rotieren.

Wenn Sie ältere API-Levels (27 und niedriger) unterstützen oder Browser mit mehreren Signaturzertifikaten verarbeiten 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) } }