Przewodnik dla deweloperów aplikacji płatniczych na Androida

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

Interfejs Payment Request API udostępnia w internecie wbudowany interfejs w przeglądarce, który umożliwia łatwiejsze niż kiedykolwiek wprowadzanie wymaganych danych karty. Interfejs API może też wywoływać aplikacje płatnicze związane z daną platformą.

Obsługa przeglądarek

  • 60
  • 15
  • 11.1

Źródło

Proces płatności w aplikacji Google Pay na danej platformie, która korzysta z płatności internetowych.

W porównaniu do korzystania wyłącznie z intencji Androida, płatności internetowe umożliwiają lepszą integrację z przeglądarką, zabezpieczeniami i wygodą użytkowników:

  • Aplikacja płatnicza jest uruchamiana jako okno modalne w kontekście witryny sprzedawcy.
  • Implementacja tej aplikacji uzupełnia istniejącą aplikację płatniczą, dzięki czemu możesz lepiej wykorzystać bazę użytkowników.
  • Podpis aplikacji płatniczej jest sprawdzany, aby zapobiec pobieraniu z innego urządzenia.
  • Aplikacje płatnicze mogą obsługiwać wiele form płatności.
  • Możesz zintegrować wszystkie formy płatności, takie jak kryptowaluty czy przelewy bankowe. Aplikacje do płatności na urządzeniach z Androidem integrują nawet formy, które wymagają dostępu do układu sprzętowego na urządzeniu.

Aby wdrożyć Płatności internetowe w aplikacji płatniczej na Androida, potrzebne są 4 kroki:

  1. Pozwól sprzedawcom znaleźć Twoją aplikację płatniczą.
  2. Poinformuj sprzedawcę, czy klient ma zarejestrowany instrument (np. kartę kredytową), który jest gotowy do dokonania płatności.
  3. Pozwól klientowi dokonać płatności.
  4. Sprawdź certyfikat podpisywania elementu wywołującego.

Aby zobaczyć, jak działają płatności internetowe, obejrzyj prezentację android-web-payment.

Krok 1. Pozwól sprzedawcom odkryć Twoją aplikację płatniczą

Aby sprzedawca mógł korzystać z Twojej aplikacji płatniczej, musi użyć interfejsu Payment Request API i określić obsługiwaną formę płatności 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 plik manifestu formy płatności, aby przeglądarki mogły znaleźć Twoją aplikację.

Krok 2. Poinformuj sprzedawcę, czy klient ma zarejestrowany instrument, który jest gotowy do płatności

Sprzedawca może wywołać hasEnrolledInstrument(), aby zapytać, czy klient może dokonać płatności. Aby odpowiedzieć na to zapytanie, możesz zaimplementować IS_READY_TO_PAY jako usługę na 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. Gdy aplikacja płatnicza nie ma takiego modułu obsługi intencji, przeglądarka przyjmuje, że aplikacja może zawsze dokonywać płatności.

AIDL

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

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

Stosowanie dyrektywy IsReadyToPayService

Najprostsza implementacja IsReadyToPayService została przedstawiona w tym przykładzie:

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

Odpowiedź

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

callback?.handleIsReadyToPay(true)

Uprawnienie

Aby sprawdzić, kto jest rozmówcą, możesz użyć narzędzia Binder.getCallingUid(). Pamiętaj, że musisz to zrobić w metodzie isReadyToPay, a nie w metodzie onBind.

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

Informacje o tym, jak sprawdzić, czy pakiet wywołujący ma właściwy podpis, znajdziesz w sekcji Sprawdzanie certyfikatu podpisywania rozmówcy.

Krok 3. Pozwól klientowi dokonać płatności

Sprzedawca wywołuje show(), aby uruchomić aplikację płatniczą, aby klient mógł dokonać płatności. Aplikacja płatnicza jest wywoływana za pomocą intencji PAY na Androida, która zawiera informacje o transakcji w parametrach intencji.

W odpowiedzi aplikacja płatnicza odpowiada za pomocą methodName i details, które są przypisane do konkretnej aplikacji płatniczej i są nieprzezroczyste dla przeglądarki. Przeglądarka przekształca ciąg znaków details w obiekt JavaScript dla sprzedawcy przez deserializację JSON, ale nie wymusza żadnej ważności poza tym. Przeglądarka nie modyfikuje parametru details. Jego wartość jest przekazywana bezpośrednio do sprzedawcy.

AndroidManifest.xml

Działanie z filtrem intencji PAY powinno zawierać tag <meta-data> identyfikujący domyślny identyfikator formy płatności aplikacji.

Aby obsługiwać wiele form płatności, dodaj tag <meta-data> z zasobem <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 musi być listą ciągów, z których każdy musi być prawidłowym, bezwzględnym adresem URL ze schematem HTTPS, jak pokazano w tym miejscu.

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

Parametry

Te parametry są przekazywane do aktywności jako dodatki związane z intencją:

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

methodNames

Nazwy używanych metod. Elementy to klucze w słowniku methodData. Aplikacja płatnicza obsługuje te formy płatności.

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

methodData

Mapowanie z każdego pola methodNames na obiekt methodData.

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

merchantName

Zawartość tagu HTML <title> na stronie płatności sprzedawcy (w kontekście przeglądania na najwyższym poziomie przeglądarki).

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

topLevelOrigin

Pochodzenie sprzedawcy bez schematu (bez schematu w kontekście przeglądania najwyższego poziomu). Na przykład https://mystore.com/checkout jest przekazywany jako mystore.com.

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

topLevelCertificateChain

Łańcuch certyfikatów sprzedawcy (łańcuch certyfikatów kontekstu przeglądania najwyższego poziomu). Wartość „null” dla hosta lokalnego i pliku na dysku, które są bezpiecznymi kontekstami bez certyfikatów SSL. Każdy Parcelable to pakiet z kluczem certificate i wartością tablicy bajtów.

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

paymentRequestOrigin

Bezschematyczne źródło kontekstu przeglądania elementu iframe, które wywołało 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.

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

total

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

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

Przykładowa zawartość ciągu znaków:

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

modifiers

Wynik funkcji JSON.stringify(details.modifiers), przy czym details.modifiers zawiera tylko supportedMethods i total.

paymentRequestId

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

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

Odpowiedź

Działanie może wysłać odpowiedź z powrotem za pomocą usługi setResult za pomocą usługi RESULT_OK.

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

Musisz określić 2 parametry jako dodatki do intencji:

  • methodName: nazwa używanej metody.
  • details: ciąg znaków JSON zawierający informacje potrzebne sprzedawcy do zrealizowania transakcji. Jeśli sukces jest wartością true, obiekt details musi być zbudowany w taki sposób, aby JSON.parse(details) zadziałał.

Możesz przekazać RESULT_CANCELED, jeśli transakcja nie została zrealizowana w aplikacji płatniczej, na przykład jeśli użytkownik nie wpisał prawidłowego kodu PIN do swojego konta w aplikacji płatniczej. Przeglądarka może pozwolić użytkownikowi wybrać inną aplikację płatniczą.

setResult(RESULT_CANCELED)
finish()

Jeśli wynik działania w odpowiedzi na płatność otrzymaną z wywołanej aplikacji płatniczej ma wartość RESULT_OK, Chrome sprawdzi, czy w jej dostosowaniach nie ma niepustych pól methodName i details. Jeśli weryfikacja się nie powiedzie, Chrome zwróci odrzuconą obietnicę z request.show() i pojawi się jeden z tych komunikatów o błędzie u dewelopera:

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

Uprawnienie

Aktywność może sprawdzić obiekt wywołujący za pomocą metody getCallingPackage().

val caller: String? = callingPackage

Ostatnim krokiem jest weryfikacja certyfikatu podpisywania rozmówcy, aby potwierdzić, że pakiet wywołujący ma właściwy podpis.

Krok 4. Sprawdź certyfikat podpisywania wywołującego

Nazwę przesyłki możesz sprawdzić w Binder.getCallingUid() w IS_READY_TO_PAY i w Activity.getCallingPackage() w PAY. Aby rzeczywiście mieć pewność, że elementem wywołującym jest przeglądarka, sprawdź jej certyfikat podpisywania i upewnij się, że jest zgodny z prawidłową wartością.

Jeśli kierujesz treści na interfejs API na poziomie 28 lub wyższym i prowadzisz integrację z przeglądarką, która ma pojedynczy certyfikat podpisywania, możesz użyć 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
)

W przypadku przeglądarek z jednym certyfikatem preferowany jest interfejs PackageManager.hasSigningCertificate(), ponieważ prawidłowo obsługuje rotację certyfikatów. Chrome ma pojedynczy certyfikat podpisywania. Aplikacje z wieloma certyfikatami podpisywania nie mogą ich obracać.

Jeśli musisz obsługiwać starsze poziomy interfejsu API 27 i starsze lub jeśli potrzebujesz obsługi przeglądarek z wieloma certyfikatami podpisywania, możesz użyć 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) } }