Entwicklerleitfaden für Android-Zahlungs-Apps

Hier erfährst du, wie du deine Android-Zahlungs-App an Web Payments anpassen und die Nutzerfreundlichkeit für deine Kunden verbessern kannst.

Die Payment Request API bietet im Web eine integrierte browserbasierte Oberfläche, über die Nutzer ihre 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 der plattformspezifischen Google Pay App, die Webzahlungen verwendet.

Verglichen mit der ausschließlichen Verwendung von Android-Intents ermöglicht Web Payments eine bessere Einbindung in den Browser sowie die Sicherheit und Nutzerfreundlichkeit:

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

Für die Implementierung von Webzahlungen in einer Android-Zahlungs-App sind vier Schritte erforderlich:

  1. Zeige Händlern deine Zahlungs-App.
  2. Teile dem Händler mit, ob ein Kunde ein registriertes Zahlungsmittel (z. B. eine Kreditkarte) hat, das zum Bezahlen bereit ist.
  3. Lassen Sie einen Kunden bezahlen.
  4. Signaturzertifikat des Aufrufers überprüfen

In der Demo android-web-payment kannst du Webzahlungen in Aktion sehen.

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

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

Wenn Sie eine eindeutige Zahlungsmethode-ID für Ihre Zahlungs-App haben, können Sie Ihr eigenes Manifest für Zahlungsmethoden einrichten, damit Browser Ihre App erkennen können.

Schritt 2: Händler informieren, ob ein Kunde ein registriertes Zahlungsmittel hat, das bezahlen kann

Der Händler kann hasEnrolledInstrument() aufrufen, um abzufragen, 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 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

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

callback?.handleIsReadyToPay(true)

Berechtigung

Sie können Binder.getCallingUid() verwenden, um zu prüfen, wer der Aufrufer ist. Beachten Sie, dass Sie dies in der Methode isReadyToPay und nicht in der Methode onBind tun müssen.

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

Unter Signaturzertifikat des Anrufers prüfen erfahren Sie, wie Sie überprüfen können, ob das aufrufende Paket die richtige Signatur hat.

Schritt 3: Einen Kunden die Zahlung ausführen lassen

Der Händler ruft show() auf, um die Zahlungs-App zu starten, damit der Kunde eine Zahlung ausführen 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 nicht transparent. Der Browser konvertiert den String details per JSON-Deserialisierung in ein JavaScript-Objekt für den Händler, erzwingt jedoch keine darüber hinausgehende Gültigkeit. Der Browser ändert den details nicht. 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 Standard-ID 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>

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

Parameters

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 methodData-Wörterbuch. Die folgenden Methoden werden von der Zahlungs-App unterstützt.

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

methodData

Eine Zuordnung von jedem der methodNames zur methodData.

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

merchantName

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

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

topLevelOrigin

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

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

topLevelCertificateChain

Die Zertifikatskette des Händlers (die Zertifikatskette des Browserkontexts der obersten Ebene). Nullwerte für „localhost“ und „Datei auf dem Laufwerk“, die beide sichere Kontexte ohne SSL-Zertifikate sind. Jeder 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 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 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 mit Push-Bezahlung mit dem Transaktionsstatus verknüpfen sollen. Händlerwebsites verwenden dieses Feld, um die Push-Bezahlungs-Apps nach dem Transaktionsstatus außerhalb des Bands abzufragen.

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

Antwort

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

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

Du musst 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 werden, dass JSON.parse(details) erfolgreich ist.

Sie können RESULT_CANCELED übergeben, wenn die Transaktion in der Zahlungs-App nicht abgeschlossen wurde, z. B. wenn der Nutzer in der Zahlungs-App nicht den richtigen PIN-Code für sein Konto eingegeben hat. Im Browser kann der Nutzer eine andere Zahlungs-App auswählen.

setResult(RESULT_CANCELED)
finish()

Wenn das Aktivitätsergebnis einer von der aufgerufenen Zahlungs-App empfangenen Zahlungsantwort auf RESULT_OK gesetzt ist, sucht Chrome in den Extras nach, ob methodName und details leer sind. Wenn die Überprüfung fehlschlägt, gibt Chrome ein abgelehntes Promise von request.show() mit einer der folgenden 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 ihrer getCallingPackage()-Methode prüfen.

val caller: String? = callingPackage

Im letzten Schritt wird anhand des Signaturzertifikats des Aufrufers überprüft, ob das aufrufende Paket die richtige Signatur hat.

Schritt 4: Signaturzertifikat des Anrufers prüfen

Sie können den Paketnamen des Aufrufers mit Binder.getCallingUid() in IS_READY_TO_PAY und mit Activity.getCallingPackage() in PAY prüfen. Um tatsächlich zu bestätigen, dass der Aufrufer der von Ihnen gewünschte Browser ist, sollten Sie das Signaturzertifikat prüfen und darauf achten, dass es mit dem richtigen Wert übereinstimmt.

Wenn Sie auf API-Level 28 oder höher ausgerichtet sind und einen Browser mit einem einzelnen Signaturzertifikat einbinden, 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 einzelnen Zertifikaten bevorzugt, da diese die Zertifikatsrotation richtig handhabt. (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) } }