Przewodnik dla deweloperów aplikacji płatniczych na Androida

Dowiedz się, jak dostosować aplikację do płatności 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 do płatności na konkretnej platformie.

Obsługa przeglądarek

  • Chrome: 60.
  • Edge: 15.
  • Firefox: za pomocą flagi.
  • Safari: 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 z wykorzystywaniem tylko intencji Androida płatności internetowe zapewniają lepszą integrację z przeglądarką, bezpieczeństwo i wygodę użytkowników:

  • Aplikacja do płatności uruchamia się jako okno modalne w kontekście strony sprzedawcy.
  • Wdrożenie jest uzupełnieniem Twojej dotychczasowej aplikacji do płatności i umożliwia korzystanie z bazy użytkowników.
  • Aby zapobiec instalowaniu aplikacji z zewnętrznych źródeł, sprawdzany jest podpis aplikacji do płatności.
  • Aplikacje płatnicze mogą obsługiwać wiele form płatności.
  • Można zintegrować dowolną formę płatności, np. kryptowalutę czy przelewy bankowe. Aplikacje do płatności na urządzeniach z Androidem mogą nawet integrować metody, które wymagają dostępu do układu sprzętowego na urządzeniu.

Aby wdrożyć płatności internetowe w aplikacji do płatności na Androida, wykonaj 4 kroki:

  1. Pozwól sprzedawcom odkryć Twoją aplikację płatniczą.
  2. Poinformuj sprzedawcę, jeśli klient ma zarejestrowany instrument płatniczy (np. kartę kredytową), który jest gotowy do zapłaty.
  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, zapoznaj się z demonstracją android-web-payment.

Krok 1. Pozwól sprzedawcom znaleźć 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 do płatności, możesz skonfigurować własny plik manifestu formy płatności, aby przeglądarki mogły wykryć Twoją aplikację.

Krok 2. Poinformuj sprzedawcę, jeśli klient ma zarejestrowany instrument, którym może zapłacić

Sprzedawca może wywołać hasEnrolledInstrument(), aby zapytać, czy klient może dokonać płatności. Aby uzyskać odpowiedź na to zapytanie, możesz zaimplementować usługę 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. Jeśli w aplikacji płatniczej nie ma takiego modułu obsługi intencji, przeglądarka zakłada, że aplikacja może zawsze dokonywać płatności.

AIDL

Interfejs API usługi IS_READY_TO_PAY jest zdefiniowany w pliku 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

Najprostsze wdrożenie IsReadyToPayService pokazano 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 dzwoniącym, możesz użyć metody Binder.getCallingUid(). Pamiętaj, że musisz to zrobić w ramach metody isReadyToPay, a nie onBind.

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

Aby dowiedzieć się, jak sprawdzić, czy pakiet wywołujący ma prawidłową sygnaturę, zapoznaj się z artykułem Weryfikowanie certyfikatu podpisującego.

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

Sprzedawca dzwoni pod numer show(), aby uruchomić aplikację do płatności, dzięki której klient może dokonać płatności. Aplikacja do płatności jest wywoływana za pomocą intencji AndroidaPAY z informacjami o transakcji w parametrach intencji.

Aplikacja do płatności odpowiada z wartościami methodNamedetails, które są specyficzne dla aplikacji do płatności i nie są widoczne dla przeglądarki. Przeglądarka zamienia ciąg details na obiekt JavaScript dla sprzedawcy za pomocą deserializacji JSON, ale nie narzuca żadnych dodatkowych wymagań dotyczących poprawności. Przeglądarka nie zmienia wartości parametru details; jest on przekazywany bezpośrednio do sprzedawcy.

AndroidManifest.xml

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

Aby obsługiwać wiele form płatności, dodaj tag <meta-data> z zasobami <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>

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

<?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 dodatkowe parametry intencji:

  • 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 każdego z parametrów methodNames na parametr methodData;

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

merchantName

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

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

topLevelOrigin

Pochodzenie sprzedawcy bez schematu (bez schematu w kontekście przeglądania najwyższego poziomu). Na przykład wartość https://mystore.com/checkout jest przekazywana 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 w przypadku localhost i pliku na dysku, które są bezpiecznymi kontekstami bez certyfikatów SSL. Każdy element 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

Źródło bez schematu w kontekście przeglądania 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")

Oto przykład treści ciągu znaków:

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

modifiers

Dane wyjściowe funkcji JSON.stringify(details.modifiers), w których details.modifiers zawiera tylko supportedMethodstotal.

paymentRequestId

Pole PaymentRequest.id, które aplikacje obsługujące „płatności push” powinny powiązać ze stanem transakcji. Strony sprzedawców będą używać tego pola do wysyłania zapytań do aplikacji „push-payment” o stan transakcji poza kanałem.

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

Odpowiedź

Aktywność może wysłać odpowiedź z poziomu setResult za pomocą 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ć skonstruowany w taki sposób, aby JSON.parse(details) zadziałał.

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

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 odrzucone zobowiązanie z request.show() z jednym z tych komunikatów o błędzie dla 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 sprawdzenie certyfikatu podpisującego, aby potwierdzić, że pakiet wywołania ma prawidłowy podpis.

Krok 4. Sprawdź certyfikat podpisywania dzwoniącego

Nazwę pakietu dzwoniącego możesz sprawdzić w polu Binder.getCallingUid()IS_READY_TO_PAY i w polu Activity.getCallingPackage()PAY. Aby potwierdzić, że wywołujący jest przeglądarką, o której myślisz, sprawdź jej certyfikat podpisywania i upewnij się, że jest on 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
)

PackageManager.hasSigningCertificate() jest preferowany w przypadku przeglądarek z pojedynczym certyfikatem, ponieważ poprawnie obsługuje rotację certyfikatów. (Chrome ma jeden certyfikat podpisywania). Aplikacje z wieloma certyfikatami podpisywania nie mogą ich obracać.

Jeśli musisz obsługiwać starsze poziomy interfejsu API 27 lub niższego albo musisz obsługiwać przeglądarki 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) } }