Przewodnik dla deweloperów aplikacji płatniczych na Androida

Dowiedz się, jak dostosować aplikację płatniczą na Androida do obsługi płatności internetowych i zapewnić klientom lepsze wrażenia.

Opublikowano: 5 maja 2020 r., ostatnia aktualizacja: 27 maja 2025 r.

Payment Request API wprowadza do internetu wbudowany interfejs oparty na przeglądarce, który umożliwia użytkownikom łatwiejsze niż kiedykolwiek wprowadzanie wymaganych informacji o płatności. Interfejs API może też wywoływać aplikacje płatnicze specyficzne dla platformy.

Browser Support

  • Chrome: 60.
  • Edge: 15.
  • Firefox: behind a flag.
  • Safari: 11.1.

Source

Proces płatności w aplikacji Google Pay specyficznej dla platformy, która korzysta z płatności internetowych.

W porównaniu z używaniem tylko intencji Androida płatności internetowe zapewniają lepszą integrację z przeglądarką, bezpieczeństwo i wygodę użytkowników:

  • Aplikacja płatnicza jest uruchamiana jako okno modalne w kontekście witryny sprzedawcy.
  • Implementacja jest uzupełnieniem istniejącej aplikacji płatniczej, co pozwala wykorzystać bazę użytkowników.
  • Podpis aplikacji płatniczej jest sprawdzany, aby zapobiec instalowaniu aplikacji z nieznanych źródeł.
  • Aplikacje płatnicze mogą obsługiwać wiele form płatności.
  • Można zintegrować dowolną formę płatności, np. kryptowalutę, przelewy bankowe itp. Aplikacje płatnicze na urządzeniach z Androidem mogą nawet integrować metody wymagające dostępu do układu sprzętowego na urządzeniu.

Wdrożenie płatności internetowych w aplikacji płatniczej na Androida wymaga 4 kroków:

  1. Umożliwienie sprzedawcom odkrywania Twojej aplikacji płatniczej.
  2. Informowanie sprzedawcy, czy klient ma zarejestrowany instrument (np. kartę kredytową), za pomocą którego może zapłacić.
  3. Umożliwienie klientowi dokonania płatności.
  4. Sprawdzenie certyfikatu podpisu źródła wywołań.

Aby zobaczyć, jak działają płatności internetowe, zapoznaj się z wersją demonstracyjną android-web-payment.

Krok 1. Umożliwienie sprzedawcom odkrywania Twojej aplikacji płatniczej

Ustaw właściwość related_applications w manifeście aplikacji internetowej zgodnie z instrukcjami w artykule Konfigurowanie formy płatności.

Aby sprzedawca mógł korzystać z Twojej aplikacji płatniczej, musi używać Payment Request API i określić formę płatności, którą obsługujesz, za pomocą identyfikatora formy płatności.

Jeśli masz identyfikator formy płatności, który jest unikalny dla Twojej aplikacji płatniczej, możesz skonfigurować własny manifest formy płatności, aby przeglądarki mogły wykrywać Twoją aplikację.

Krok 2. Informowanie sprzedawcy, czy klient ma zarejestrowany instrument, za pomocą którego może zapłacić

Sprzedawca może wywołać hasEnrolledInstrument(), aby sprawdzić, czy klient może dokonać płatności. Aby odpowiedzieć na to zapytanie, możesz zaimplementować IS_READY_TO_PAY jako usługę Androida.

AndroidManifest.xml

Zadeklaruj usługę za pomocą filtra intencji z działaniem 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>

Usługa IS_READY_TO_PAY jest opcjonalna. Jeśli w aplikacji płatniczej nie ma takiego modułu obsługi intencji, przeglądarka internetowa zakłada, że aplikacja zawsze może dokonywać płatności.

AIDL

Interfejs API usługi IS_READY_TO_PAY jest zdefiniowany w AIDL. Utwórz 2 pliki AIDL z tą treścią:

org/chromium/IsReadyToPayServiceCallback.aidl

package org.chromium;

interface IsReadyToPayServiceCallback {
    oneway void handleIsReadyToPay(boolean isReadyToPay);
}

org/chromium/IsReadyToPayService.aidl

package org.chromium;

import org.chromium.IsReadyToPayServiceCallback;

interface IsReadyToPayService {
    oneway void isReadyToPay(IsReadyToPayServiceCallback callback, in Bundle parameters);
}

Implementowanie IsReadyToPayService

Najprostsza implementacja IsReadyToPayService jest pokazana w tym przykładzie:

Kotlin

class SampleIsReadyToPayService : Service() {
    private val binder = object : IsReadyToPayService.Stub() {
        override fun isReadyToPay(callback: IsReadyToPayServiceCallback?, parameters: Bundle?) {
            callback?.handleIsReadyToPay(true)
        }
    }

    override fun onBind(intent: Intent?): IBinder? {
        return binder
    }
}

Java

import org.chromium.IsReadyToPayService;

public class SampleIsReadyToPayService extends Service {
    private final IsReadyToPayService.Stub mBinder =
        new IsReadyToPayService.Stub() {
            @Override
            public void isReadyToPay(IsReadyToPayServiceCallback callback, Bundle parameters) {
                if (callback != null) {
                    callback.handleIsReadyToPay(true);
                }
            }
        };

    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }
}

Odpowiedź

Usługa może wysłać odpowiedź za pomocą metody handleIsReadyToPay(Boolean).

Kotlin

callback?.handleIsReadyToPay(true)

Java

if (callback != null) {
    callback.handleIsReadyToPay(true);
}

Uprawnienie

Aby sprawdzić, kto jest elementem wywołującym, możesz użyć Binder.getCallingUid(). Pamiętaj, że musisz to zrobić w metodzie isReadyToPay, a nie w metodzie onBind, ponieważ system operacyjny Android może buforować i ponownie używać połączenia z usługą, co nie powoduje wywołania metody onBind().

Kotlin

override fun isReadyToPay(callback: IsReadyToPayServiceCallback?, parameters: Bundle?) {
    try {
        val untrustedPackageName = parameters?.getString("packageName")
        val actualPackageNames = packageManager.getPackagesForUid(Binder.getCallingUid())
        // ...

Java

@Override
public void isReadyToPay(IsReadyToPayServiceCallback callback, Bundle parameters) {
    try {
        String untrustedPackageName = parameters != null
                ? parameters.getString("packageName")
                : null;
        String[] actualPackageNames = packageManager.getPackagesForUid(Binder.getCallingUid());
        // ...

Podczas odbierania wywołań IPC zawsze sprawdzaj, czy parametry wejściowe nie są równe null. Jest to szczególnie ważne, ponieważ różne wersje lub rozwidlenia systemu operacyjnego Android mogą zachowywać się w nieoczekiwany sposób i powodować błędy, jeśli nie zostaną obsłużone.

Chociaż packageManager.getPackagesForUid() zwykle zwraca pojedynczy element, Twój kod musi obsługiwać rzadki przypadek, w którym źródło wywołań używa wielu nazw pakietów. Dzięki temu Twoja aplikacja pozostanie niezawodna.

Więcej informacji o tym, jak sprawdzić, czy pakiet wywołujący ma prawidłowy podpis, znajdziesz w artykule Sprawdzanie certyfikatu podpisu źródła wywołań o tym, jak sprawdzić, czy pakiet wywołujący ma prawidłowy podpis.

Parametry

Pakiet parameters został dodany w Chrome 139. Zawsze należy sprawdzać, czy nie jest on równy null.

Do usługi w pakiecie parameters przekazywane są te parametry:

  • packageName
  • methodNames
  • methodData
  • topLevelOrigin
  • paymentRequestOrigin
  • topLevelCertificateChain

Parametr packageName został dodany w Chrome 138. Zanim użyjesz jego wartości, musisz sprawdzić ten parametr pod kątem Binder.getCallingUid(). Ta weryfikacja jest niezbędna, ponieważ pakiet parameters jest w pełni kontrolowany przez element wywołujący, a Binder.getCallingUid() – przez system operacyjny Android.

W WebView i w witrynach innych niż HTTPS, które są zwykle używane do testowania lokalnego, np. http://localhost, parametr topLevelCertificateChain ma wartość null.

Krok 3. Umożliwienie klientowi dokonania płatności

Sprzedawca wywołuje show(), aby uruchomić aplikację płatniczą , dzięki czemu klient może dokonać płatności. Aplikacja płatnicza jest wywoływana za pomocą intencji Androida PAY z informacjami o transakcji w parametrach intencji.

Aplikacja płatnicza odpowiada za pomocą methodName i details, które są specyficzne dla aplikacji płatniczej i nie są widoczne dla przeglądarki. Przeglądarka konwertuje ciąg details na słownik JavaScriptu dla sprzedawcy za pomocą deserializacji ciągu JSON, ale nie wymusza żadnej innej weryfikacji. Przeglądarka nie modyfikuje details; wartość tego parametru jest przekazywana bezpośrednio do sprzedawcy.

AndroidManifest.xml

Aktywność z filtrem intencji PAY powinna mieć tag <meta-data> który identyfikuje domyślny identyfikator formy płatności dla aplikacji.

Aby obsługiwać wiele form płatności, dodaj tag <meta-data> z <string-array> zasobem.

<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/chromium_payment_method_names" />
</activity>

android:resource musi być listą ciągów znaków, z których każdy musi być prawidłowym, bezwzględnym adresem URL ze schematem HTTPS, jak pokazano tutaj.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="chromium_payment_method_names">
        <item>https://alicepay.com/put/optional/path/here</item>
        <item>https://charliepay.com/put/optional/path/here</item>
    </string-array>
</resources>

Parametry

Te parametry są przekazywane do aktywności jako dodatki Intent:

  • methodNames
  • methodData
  • merchantName
  • topLevelOrigin
  • topLevelCertificateChain
  • paymentRequestOrigin
  • total
  • modifiers
  • paymentRequestId
  • paymentOptions
  • shippingOptions

Kotlin

val extras: Bundle? = getIntent()?.extras

Java

Bundle extras = getIntent() != null ? getIntent().getExtras() : null;

methodNames

Nazwy używanych metod. Elementy są kluczami w słowniku methodData. Są to metody obsługiwane przez aplikację płatniczą.

Kotlin

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

Java

List<String> methodNames = extras.getStringArrayList("methodNames");

methodData

Mapowanie każdej z methodNames na methodData.

Kotlin

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

Java

Bundle methodData = extras.getBundle("methodData");

merchantName

Zawartość tagu HTML <title> na stronie płatności sprzedawcy (kontekst przeglądania najwyższego poziomu przeglądarki ).

Kotlin

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

Java

String merchantName = extras.getString("merchantName");

topLevelOrigin

Źródło sprzedawcy bez schematu (źródło bez schematu kontekstu przeglądania najwyższego poziomu). Na przykład https://mystore.com/checkout jest przekazywane jako mystore.com.

Kotlin

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

Java

String topLevelOrigin = extras.getString("topLevelOrigin");

topLevelCertificateChain

Łańcuch certyfikatów sprzedawcy (łańcuch certyfikatów kontekstu przeglądania najwyższego poziomu). Wartość to null w przypadku WebView, localhost lub pliku na dysku. Każdy Parcelable to pakiet z kluczem certificate i wartością tablicy bajtów.

Kotlin

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

Java

Parcelable[] topLevelCertificateChain =
        extras.getParcelableArray("topLevelCertificateChain");
if (topLevelCertificateChain != null) {
    for (Parcelable p : topLevelCertificateChain) {
        if (p != null && p instanceof Bundle) {
            ((Bundle) p).getByteArray("certificate");
        }
    }
}

paymentRequestOrigin

Źródło bez schematu kontekstu przeglądania elementu iframe, który wywołał konstruktor new PaymentRequest(methodData, details, options) w JavaScript. Jeśli konstruktor został wywołany z kontekstu najwyższego poziomu, wartość tego parametru jest równa wartości parametru topLevelOrigin.

Kotlin

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

Java

String paymentRequestOrigin = extras.getString("paymentRequestOrigin");

total

Ciąg JSON reprezentujący łączną kwotę transakcji.

Kotlin

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

Java

String total = extras.getString("total");

Oto przykład zawartości ciągu:

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

modifiers

Wynik JSON.stringify(details.modifiers), gdzie details.modifiers zawierają tylko supportedMethods, data i total.

paymentRequestId

Pole PaymentRequest.id, które aplikacje „push-payment” powinny powiązać ze stanem transakcji. Witryny sprzedawców będą używać tego pola do wysyłania zapytań do aplikacji „push-payment” o stan transakcji poza pasmem.

Kotlin

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

Java

String paymentRequestId = extras.getString("paymentRequestId");

Odpowiedź

Aktywność może wysłać odpowiedź za pomocą setResult z RESULT_OK.

Kotlin

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

Java

Intent result = new Intent();
Bundle extras = new Bundle();
extras.putString("methodName", "https://bobbucks.dev/pay");
extras.putString("details", "{\"token\": \"put-some-data-here\"}");
result.putExtras(extras);
setResult(Activity.RESULT_OK, result);
finish();

Musisz określić 2 parametry jako dodatki intencji:

  • methodName: nazwa używanej metody.
  • details: ciąg JSON zawierający informacje niezbędne sprzedawcy do sfinalizowania transakcji. Jeśli success ma wartość true, details muszą być skonstruowane w taki sposób, aby JSON.parse(details) zakończyło się powodzeniem. Jeśli nie ma danych, które trzeba zwrócić, ten ciąg może mieć postać "{}", który witryna sprzedawcy otrzyma jako pusty słownik JavaScriptu.

Jeśli użytkownik anuluje transakcję w aplikacji płatniczej, możesz przekazać RESULT_CANCELED. Spowoduje to, że request.show() odrzuci AbortError w witrynie sprzedawcy, wskazując na anulowanie przez użytkownika.

Kotlin

setResult(Activity.RESULT_CANCELED)
finish()

Java

setResult(Activity.RESULT_CANCELED);
finish();

Od Chrome 149 obsługiwane są te wartości wyników:

Kotlin

Activity.RESULT_CANCELED // 0 (0x00000000)
Activity.RESULT_OK // -1 (0xffffffff)
const val INTERNAL_PAYMENT_APP_ERROR = Activity.RESULT_FIRST_USER // 1 (0x00000001)

Java

Activity.RESULT_CANCELED // 0 (0x00000000)
Activity.RESULT_OK // -1 (0xffffffff)
static final int INTERNAL_PAYMENT_APP_ERROR = Activity.RESULT_FIRST_USER; // 1 (0x00000001)

Jeśli aplikacja płatnicza nie działa z powodu błędu wewnętrznego, możesz to wskazać, przekazując Activity.RESULT_FIRST_USER jako kod wyniku.

Jeśli zostanie zwrócony INTERNAL_PAYMENT_APP_ERROR, request.show() odrzuci OperationError w witrynie sprzedawcy, wskazując na błąd w aplikacji płatniczej.

To rozróżnienie między RESULT_CANCELED (0) w przypadku anulowania przez użytkownika, które powoduje AbortError, a INTERNAL_PAYMENT_APP_ERROR (1) w przypadku błędu wewnętrznego aplikacji, który powoduje OperationError, umożliwia sprzedawcom tworzenie lepszych ścieżek użytkownika.

Kotlin

setResult(Activity.RESULT_FIRST_USER)
finish()

Java

setResult(Activity.RESULT_FIRST_USER);
finish();

Jeśli wynik aktywności odpowiedzi płatności otrzymanej z wywołanej aplikacji płatniczej jest ustawiony na RESULT_OK, Chrome sprawdzi, czy w dodatkach nie ma pustych wartości methodName i details. Jeśli weryfikacja się nie powiedzie, Chrome zwróci odrzuconą obietnicę z request.show() z jednym z tych komunikatów o błędach dla deweloperów:

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

Uprawnienie

Aktywność może sprawdzić źródło wywołań za pomocą metody getCallingPackage().

Kotlin

val caller: String? = callingPackage

Java

String caller = getCallingPackage();

Ostatnim krokiem jest sprawdzenie certyfikatu podpisu źródła wywołań, aby potwierdzić, że pakiet wywołujący ma prawidłowy podpis.

Krok 4. Sprawdzenie certyfikatu podpisu źródła wywołań

Możesz sprawdzić nazwę pakietu źródła wywołań za pomocą Binder.getCallingUid() w IS_READY_TO_PAY, i za pomocą Activity.getCallingPackage() w PAY. Aby sprawdzić, czy źródłem wywołań jest przeglądarka, którą masz na myśli, sprawdź jej certyfikat podpisu i upewnij się, że pasuje do prawidłowej wartości.

Jeśli kierujesz reklamy na interfejs API na poziomie 28 lub wyższym i integrujesz się z przeglądarką, która ma jeden certyfikat podpisu, możesz użyć PackageManager.hasSigningCertificate().

Kotlin

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
)

Java

String packageName =  // The caller's package name
byte[] certificate =  // The correct signing certificate
boolean verified = packageManager.hasSigningCertificate(
        callingPackage,
        certificate,
        PackageManager.CERT_INPUT_SHA256);

PackageManager.hasSigningCertificate() jest preferowany w przypadku przeglądarek z jednym certyfikatem, ponieważ prawidłowo obsługuje rotację certyfikatów. (Chrome ma jeden certyfikat podpisu). Aplikacje, które mają wiele certyfikatów podpisu, nie mogą ich obracać.

Jeśli musisz obsługiwać interfejsy API na poziomie 27 i niższym lub jeśli musisz obsługiwać przeglądarki z wieloma certyfikatami podpisu, możesz użyć PackageManager.GET_SIGNATURES.

Kotlin

val packageName: String =  // The caller's package name
val expected: Set<String> =  // The correct set of signing certificates

val packageInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES)
val sha256 = MessageDigest.getInstance("SHA-256")
val actual = packageInfo.signatures.map {
    SerializeByteArrayToString(sha256.digest(it.toByteArray()))
}
val verified = actual.equals(expected)

Java

String packageName  =  // The caller's package name
Set<String> expected =  // The correct set of signing certificates

PackageInfo packageInfo =
        packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
Set<String> actual = new HashSet<>();
for (Signature it : packageInfo.signatures) {
    actual.add(SerializeByteArrayToString(sha256.digest(it.toByteArray())));
}
boolean verified = actual.equals(expected);

Debugowanie

Aby obserwować błędy lub komunikaty informacyjne, użyj tego polecenia:

adb logcat | grep -i pay