Guia para desenvolvedores de apps de pagamento do Android

Saiba como adaptar seu app de pagamento para Android para funcionar com o Web Payments e proporcionar uma melhor experiência do usuário aos clientes.

A API Payment Request traz para a Web uma interface integrada baseada em navegador que permite aos usuários inserir as informações de pagamento necessárias com mais facilidade do que nunca. A API também pode invocar apps de pagamento específicos da plataforma.

Compatibilidade com navegadores

  • 60
  • 15
  • 11.1

Origem

Fluxo de finalização de compra com o app Google Pay específico da plataforma que usa pagamentos na Web.

Em comparação com o uso apenas de intents do Android, o Web Payments permite uma melhor integração com o navegador, a segurança e a experiência do usuário:

  • O app de pagamento é iniciado como um modal, no contexto do site do comerciante.
  • A implementação é complementar ao app de pagamento atual, permitindo que você aproveite sua base de usuários.
  • A assinatura do app de pagamento é verificada para evitar transferências por sideload.
  • Os apps de pagamento aceitam várias formas de pagamento.
  • Qualquer forma de pagamento, como criptomoedas, transferências bancárias e muito mais, pode ser integrada. Os apps de pagamento em dispositivos Android podem até mesmo integrar métodos que exigem acesso ao chip de hardware do dispositivo.

São necessárias quatro etapas para implementar pagamentos na Web em um app de pagamento para Android:

  1. Permita que os comerciantes descubram seu app de pagamento.
  2. Informe ao comerciante se um cliente tem um instrumento registrado (como um cartão de crédito) pronto para pagar.
  3. Permitir que um cliente faça um pagamento.
  4. Verifique o certificado de assinatura do autor da chamada.

Para ver o Web Payments em ação, confira a demonstração android-web-payment.

Etapa 1: permitir que os comerciantes descubram seu app de pagamento

Para que um comerciante use seu app de pagamento, ele precisa utilizar a API Payment Request e especificar a forma de pagamento aceita usando o identificador da forma de pagamento.

Se você tiver um identificador de forma de pagamento exclusivo do seu app de pagamento, configure seu próprio manifesto de forma de pagamento para que os navegadores possam descobrir seu app.

Etapa 2: informar ao comerciante se um cliente tem um instrumento registrado pronto para pagamento

O comerciante pode chamar hasEnrolledInstrument() para consultar se o cliente pode fazer um pagamento. Você pode implementar IS_READY_TO_PAY como um serviço Android para responder a essa consulta.

AndroidManifest.xml

Declare o serviço com um filtro de intent com a ação 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>

O serviço IS_READY_TO_PAY é opcional. Se não houver esse gerenciador de intents no app de pagamento, o navegador da Web presumirá que o app sempre pode fazer pagamentos.

AIDL

A API do serviço IS_READY_TO_PAY é definida na AIDL. Crie dois arquivos AIDL com o seguinte conteúdo:

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

Implementação de IsReadyToPayService

A implementação mais simples de IsReadyToPayService é mostrada no exemplo abaixo:

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

Resposta

O serviço pode enviar a resposta pelo método handleIsReadyToPay(Boolean).

callback?.handleIsReadyToPay(true)

Permissão

Use Binder.getCallingUid() para verificar quem é o autor da chamada. Observe que você precisa fazer isso no método isReadyToPay, não no método onBind.

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

Consulte Verificar o certificado de assinatura do autor da chamada para saber como verificar se o pacote de chamada tem a assinatura correta.

Etapa 3: permitir que um cliente faça um pagamento

O comerciante chama show() para iniciar o app de pagamento para que o cliente possa fazer um pagamento. O app de pagamento é invocado por uma intent do Android PAY com informações da transação nos parâmetros da intent.

O app de pagamento responde com methodName e details, que são específicos desse app e são opacos para o navegador. O navegador converte a string details em um objeto JavaScript para o comerciante por meio da desserialização JSON, mas não impõe nenhuma validade além disso. O navegador não modifica o details. O valor desse parâmetro é transmitido diretamente ao comerciante.

AndroidManifest.xml

A atividade com o filtro de intent PAY precisa ter uma tag <meta-data> que identifique o identificador da forma de pagamento padrão do app.

Para aceitar várias formas de pagamento, adicione uma tag <meta-data> com um recurso <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>

O resource precisa ser uma lista de strings, cada uma delas um URL absoluto válido e com um esquema HTTPS, conforme mostrado aqui.

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

Parâmetros

Os parâmetros a seguir são passados para a atividade como extras de intent:

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

methodNames

Os nomes dos métodos que estão sendo usados. Os elementos são as chaves no dicionário methodData. Essas são as formas de pagamento compatíveis com o app.

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

methodData

Um mapeamento de cada um dos methodNames para o methodData.

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

merchantName

O conteúdo da tag HTML <title> da página de finalização da compra do comerciante (o contexto de navegação de nível superior do navegador).

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

topLevelOrigin

A origem do comerciante sem o esquema (a origem sem esquema do contexto de navegação de nível superior). Por exemplo, https://mystore.com/checkout é transmitido como mystore.com.

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

topLevelCertificateChain

A cadeia de certificados do comerciante (a cadeia de certificados do contexto de navegação de nível superior). Nulo para localhost e arquivo em disco, que são contextos seguros sem certificados SSL. Cada Parcelable é um pacote com uma chave certificate e um valor de matriz de bytes.

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

paymentRequestOrigin

A origem sem esquema do contexto de navegação iframe que invocou o construtor new PaymentRequest(methodData, details, options) no JavaScript. Se o construtor tiver sido invocado do contexto de nível superior, o valor desse parâmetro será igual ao valor do parâmetro topLevelOrigin.

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

total

A string JSON que representa o valor total da transação.

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

Confira um exemplo de conteúdo da string:

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

modifiers

A saída de JSON.stringify(details.modifiers), em que details.modifiers contém apenas supportedMethods e total.

paymentRequestId

O campo PaymentRequest.id que os apps de "push-payment" associam ao estado da transação. Os sites do comerciante usarão esse campo para consultar os apps de pagamento por push sobre o estado da transação fora da banda.

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

Resposta

A atividade pode enviar a resposta de volta pelo setResult com RESULT_OK.

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

Especifique dois parâmetros como extras de intent:

  • methodName: o nome do método que está sendo usado.
  • details: string JSON que contém as informações necessárias para o comerciante concluir a transação. Se o sucesso for true, details precisará ser construído de forma que JSON.parse(details) seja bem-sucedido.

Você pode transmitir RESULT_CANCELED se a transação não tiver sido concluída no app de pagamento, por exemplo, se o usuário não digitar o código PIN correto da conta no app de pagamento. O navegador pode permitir que o usuário escolha outro app de pagamento.

setResult(RESULT_CANCELED)
finish()

Se o resultado da atividade de uma resposta de pagamento recebida do app de pagamento invocado estiver definido como RESULT_OK, o Chrome vai procurar methodName e details não vazios nos extras. Se a validação falhar, o Chrome vai retornar uma promessa rejeitada de request.show() com uma das seguintes mensagens de erro do desenvolvedor:

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

Permissão

A atividade pode verificar o autor da chamada com o método getCallingPackage().

val caller: String? = callingPackage

A etapa final é verificar o certificado de assinatura do autor da chamada para confirmar se o pacote de chamada tem a assinatura correta.

Etapa 4: verificar o certificado de assinatura do autor da chamada

É possível conferir o nome do pacote do autor da chamada com Binder.getCallingUid() em IS_READY_TO_PAY e com Activity.getCallingPackage() em PAY. Para realmente verificar se o autor da chamada é o navegador que você tem em mente, confira o certificado de assinatura dele e confira se ele corresponde ao valor correto.

Se você estiver segmentando a API de nível 28 ou mais recente e estiver fazendo a integração com um navegador que tenha um único certificado de assinatura, use 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() é preferível para navegadores de certificado único, porque ele processa corretamente a rotação de certificados. O Chrome tem um único certificado de assinatura. Os apps que têm vários certificados de assinatura não podem alterá-los.

Se você precisar de compatibilidade com níveis de API anteriores ao 27 ou versões anteriores ou precisar processar navegadores com vários certificados de assinatura, use 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) } }