Guide du développeur d'applications de paiement Android

Découvrez comment adapter votre application de paiement Android pour qu'elle fonctionne avec les paiements Web et offrir une meilleure expérience utilisateur à vos clients.

Publié le 5 mai 2020, dernière mise à jour le 27 mai 2025

L'API Payment Request propose sur le Web une interface intégrée basée sur le navigateur qui permet aux utilisateurs de saisir les informations de paiement requises plus facilement que jamais. L'API peut également appeler des applications de paiement propres à la plate-forme.

Browser Support

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

Source

Flux de paiement avec une application Google Pay spécifique à la plate-forme qui utilise les paiements Web.

Par rapport à l'utilisation d'intents Android uniquement, les paiements Web permettent une meilleure intégration avec le navigateur, la sécurité et l'expérience utilisateur:

  • L'application de paiement est lancée en tant que fenêtre modale, dans le contexte du site Web du marchand.
  • L'implémentation est complémentaire de votre application de paiement existante, ce qui vous permet de tirer parti de votre base d'utilisateurs.
  • La signature de l'application de paiement est vérifiée pour éviter le sideloading.
  • Les applications de paiement peuvent accepter plusieurs modes de paiement.
  • Vous pouvez intégrer n'importe quel mode de paiement, comme les cryptomonnaies, les virements bancaires, etc. Les applications de paiement sur les appareils Android peuvent même intégrer des méthodes qui nécessitent un accès à la puce matérielle de l'appareil.

L'implémentation des paiements Web dans une application de paiement Android se déroule en quatre étapes:

  1. Faites découvrir votre application de paiement aux marchands.
  2. Indiquez au marchand si un client dispose d'un mode de paiement enregistré (comme une carte de crédit) prêt à payer.
  3. Autorisez un client à effectuer un paiement.
  4. Vérifiez le certificat de signature de l'appelant.

Pour voir les paiements Web en action, consultez la démonstration android-web-payment.

Étape 1: Laissez les marchands découvrir votre application de paiement

Définissez la propriété related_applications dans le fichier manifeste de l'application Web en suivant les instructions de la section Configurer un mode de paiement.

Pour qu'un marchand puisse utiliser votre application de paiement, il doit utiliser l'API Payment Request et spécifier le mode de paiement que vous acceptez à l'aide de l'identifiant du mode de paiement.

Si vous disposez d'un identifiant de mode de paiement propre à votre application de paiement, vous pouvez configurer votre propre fichier manifeste de mode de paiement afin que les navigateurs puissent découvrir votre application.

Étape 2: Indiquer à un marchand si un client dispose d'un instrument enregistré prêt à payer

Le marchand peut appeler hasEnrolledInstrument() pour s'interroger sur la possibilité pour le client d'effectuer un paiement. Vous pouvez implémenter IS_READY_TO_PAY en tant que service Android pour répondre à cette requête.

AndroidManifest.xml

Déclarez votre service avec un filtre d'intent avec l'action 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>

Le service IS_READY_TO_PAY est facultatif. Si aucun tel gestionnaire d'intent n'est défini dans l'application de paiement, le navigateur Web suppose que l'application peut toujours effectuer des paiements.

AIDL

L'API du service IS_READY_TO_PAY est définie dans AIDL. Créez deux fichiers AIDL avec le contenu suivant:

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

Mettre en œuvre la directive IsReadyToPayService

L'implémentation la plus simple de IsReadyToPayService est illustrée dans l'exemple suivant:

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

Réponse

Le service peut envoyer sa réponse à l'aide de la méthode handleIsReadyToPay(Boolean).

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

Autorisation

Vous pouvez utiliser Binder.getCallingUid() pour vérifier l'identité de l'appelant. Notez que vous devez le faire dans la méthode isReadyToPay, et non dans la méthode onBind, car Android OS peut mettre en cache et réutiliser la connexion de service, ce qui ne déclenche pas la méthode onBind().

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

Vérifiez toujours les paramètres d'entrée pour null lorsque vous recevez des appels de communication inter-processus (IPC). Cela est particulièrement important, car différentes versions ou fourches du système d'exploitation Android peuvent se comporter de manière inattendue et entraîner des erreurs si elles ne sont pas gérées.

Bien que packageManager.getPackagesForUid() renvoie généralement un seul élément, votre code doit gérer le scénario inhabituel où un appelant utilise plusieurs noms de packages. Cela garantit que votre application reste robuste.

Consultez Vérifier le certificat de signature de l'appelant pour savoir comment vérifier que le package appelant dispose de la signature appropriée.

Paramètres

Le bundle parameters a été ajouté dans Chrome 139. Il doit toujours être comparé à null.

Les paramètres suivants sont transmis au service dans le bundle parameters:

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

packageName a été ajouté dans Chrome 138. Vous devez vérifier ce paramètre auprès de Binder.getCallingUid() avant d'utiliser sa valeur. Cette validation est essentielle, car le bundle parameters est entièrement sous le contrôle de l'appelant, tandis que Binder.getCallingUid() est contrôlé par l'OS Android.

topLevelCertificateChain est null dans WebView et sur les sites Web non-https généralement utilisés pour les tests en local, tels que http://localhost.

Étape 3: Autorisez un client à effectuer un paiement

Le marchand appelle show() pour lancer l'application de paiement afin que le client puisse effectuer un paiement. L'application de paiement est appelée à l'aide d'un intent Android PAY avec des informations de transaction dans les paramètres d'intent.

L'application de paiement répond avec methodName et details, qui sont spécifiques à l'application de paiement et opaques pour le navigateur. Le navigateur convertit la chaîne details en dictionnaire JavaScript pour le marchand à l'aide de la désérialisation de la chaîne JSON, mais n'applique aucune validité au-delà. Le navigateur ne modifie pas details. La valeur de ce paramètre est transmise directement au marchand.

AndroidManifest.xml

L'activité avec le filtre d'intent PAY doit comporter une balise <meta-data> qui identifie l'identifiant du mode de paiement par défaut de l'application.

Pour prendre en charge plusieurs modes de paiement, ajoutez une balise <meta-data> avec une ressource <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/chromium_payment_method_names" />
</activity>

android:resource doit être une liste de chaînes, chacune d'elles devant être une URL absolue valide avec un schéma HTTPS, comme illustré ici.

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

Paramètres

Les paramètres suivants sont transmis à l'activité en tant qu'extras Intent:

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

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

methodNames

Noms des méthodes utilisées. Les éléments sont les clés du dictionnaire methodData. Il s'agit des méthodes acceptées par l'application de paiement.

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

methodData

Mappage de chacun des methodNames sur le methodData.

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

merchantName

Contenu de la balise HTML <title> de la page de paiement du marchand (contexte de navigation de premier niveau du navigateur).

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

topLevelOrigin

Origine du marchand sans le schéma (origine sans schéma du contexte de navigation de premier niveau). Par exemple, https://mystore.com/checkout est transmis en tant que mystore.com.

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

topLevelCertificateChain

Chaîne de certificats du marchand (chaîne de certificats du contexte de navigation de premier niveau). La valeur est null pour WebView, localhost ou un fichier sur un disque. Chaque Parcelable est un bundle avec une clé certificate et une valeur de tableau d'octets.

val topLevelCertificateChain: Array<Parcelable>? =
        extras.getParcelableArray("topLevelCertificateChain")
val list: List<ByteArray>? = topLevelCertificateChain?.mapNotNull { p ->
    (p as Bundle).getByteArray("certificate")
}
Parcelable[] topLevelCertificateChain =
        extras.getParcelableArray("topLevelCertificateChain");
if (topLevelCertificateChain != null) {
    for (Parcelable p : topLevelCertificateChain) {
        if (p != null && p instanceof Bundle) {
            ((Bundle) p).getByteArray("certificate");
        }
    }
}

paymentRequestOrigin

Origine sans schéma du contexte de navigation de l'iFrame qui a appelé le constructeur new PaymentRequest(methodData, details, options) en JavaScript. Si le constructeur a été appelé à partir du contexte de niveau supérieur, la valeur de ce paramètre est égale à celle du paramètre topLevelOrigin.

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

total

Chaîne JSON représentant le montant total de la transaction.

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

Voici un exemple de contenu de la chaîne:

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

modifiers

Sortie de JSON.stringify(details.modifiers), où details.modifiers ne contient que supportedMethods, data et total.

paymentRequestId

Champ PaymentRequest.id que les applications de paiement par transfert doivent associer à l'état de la transaction. Les sites Web des marchands utiliseront ce champ pour interroger les applications de paiement par transfert hors bande sur l'état de la transaction.

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

Réponse

L'activité peut renvoyer sa réponse via setResult avec RESULT_OK.

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

Vous devez spécifier deux paramètres en tant qu'extras d'intent:

  • methodName: nom de la méthode utilisée.
  • details: chaîne JSON contenant les informations nécessaires au marchand pour effectuer la transaction. Si la réussite est true, details doit être construit de manière à ce que JSON.parse(details) réussisse. Si aucune donnée ne doit être renvoyée, cette chaîne peut être "{}", que le site Web du marchand recevra comme un dictionnaire JavaScript vide.

Vous pouvez transmettre RESULT_CANCELED si la transaction n'a pas été effectuée dans l'application de paiement, par exemple si l'utilisateur n'a pas saisi le bon code PIN pour son compte dans l'application de paiement. Le navigateur peut autoriser l'utilisateur à choisir une autre application de paiement.

setResult(Activity.RESULT_CANCELED)
finish()
setResult(Activity.RESULT_CANCELED);
finish();

Si le résultat de l'activité d'une réponse de paiement reçue de l'application de paiement appelée est défini sur RESULT_OK, Chrome recherche des methodName et details non vides dans ses extras. Si la validation échoue, Chrome renvoie une promesse refusée de request.show() avec l'un des messages d'erreur destinés aux développeurs suivants:

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

Autorisation

L'activité peut vérifier l'appelant à l'aide de sa méthode getCallingPackage().

val caller: String? = callingPackage
String caller = getCallingPackage();

La dernière étape consiste à vérifier le certificat de signature de l'appelant pour s'assurer que le package appelant dispose de la signature appropriée.

Étape 4: Vérifier le certificat de signature de l'appelant

Vous pouvez vérifier le nom du package de l'appelant avec Binder.getCallingUid() dans IS_READY_TO_PAY et avec Activity.getCallingPackage() dans PAY. Pour vérifier que l'appelant est bien le navigateur que vous avez en tête, vous devez vérifier son certificat de signature et vous assurer qu'il correspond à la valeur correcte.

Si vous ciblez le niveau d'API 28 ou version ultérieure et que vous vous intégrez à un navigateur disposant d'un seul certificat de signature, vous pouvez utiliser 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
)
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() est préférable pour les navigateurs à certificat unique, car il gère correctement la rotation des certificats. (Chrome dispose d'un seul certificat de signature.) Les applications qui disposent de plusieurs certificats de signature ne peuvent pas les faire pivoter.

Si vous devez prendre en charge les niveaux d'API 27 et versions antérieures, ou si vous devez gérer des navigateurs avec plusieurs certificats de signature, vous pouvez utiliser PackageManager.GET_SIGNATURES.

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

Déboguer

Utilisez la commande suivante pour observer les erreurs ou les messages d'information:

adb logcat | grep -i pay