دليل مطوّري تطبيقات الدفع المتوافقة مع Android

تعرَّف على كيفية تكييف تطبيق الدفع على Android للعمل مع "الدفع على الويب" وتوفير تجربة أفضل للمستخدمين.

تاريخ النشر: 5 مايو 2020، تاريخ آخر تعديل: 27 مايو 2025

توفّر Payment Request API على الويب واجهة مدمجة مستندة إلى المتصفّح تتيح للمستخدمين إدخال معلومات الدفع المطلوبة بسهولة لم يسبق لها مثيل. يمكن لواجهة برمجة التطبيقات أيضًا استدعاء تطبيقات الدفع الخاصة بالمنصة.

Browser Support

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

Source

مسار الدفع باستخدام تطبيق Google Pay الخاص بالمنصة والذي يستخدِم "الدفع على الويب"

مقارنةً باستخدام Android Intents فقط، يتيح "الدفع على الويب" إمكانية التكامل بشكل أفضل مع المتصفّح والأمان وتجربة المستخدم:

  • يتم تشغيل تطبيق الدفع كنافذة مشروطة في سياق الموقع الإلكتروني للتاجر.
  • يُعدّ التنفيذ مكمّلاً لتطبيق الدفع الحالي، ما يتيح لك الاستفادة من قاعدة المستخدمين.
  • يتم التحقّق من توقيع تطبيق الدفع لمنع التحميل الجانبي.
  • يمكن أن تتيح تطبيقات الدفع طرق دفع متعدّدة.
  • يمكن دمج أي طريقة دفع، مثل العملات المشفّرة والتحويلات المصرفية والمزيد. يمكن أن تدمج تطبيقات الدفع على أجهزة Android طرقًا تتطلّب الوصول إلى شريحة الأجهزة.

يتطلّب تنفيذ "الدفع على الويب" في تطبيق دفع على Android أربع خطوات:

  1. السماح للتجار باكتشاف تطبيق الدفع
  2. إبلاغ التاجر إذا كان لدى العميل أداة دفع مسجّلة (مثل بطاقة الائتمان) جاهزة للدفع
  3. السماح للعميل بإجراء الدفع
  4. التحقّق من شهادة التوقيع الخاصة بالبرنامج الذي يطلب الزحف

للاطّلاع على "الدفع على الويب" أثناء العمل، يمكنك الاطّلاع على العرض التوضيحي android-web-payment.

الخطوة 1: السماح للتجار باكتشاف تطبيق الدفع

اضبط السمة related_applications في بيان تطبيق الويب وفقًا للـ تعليمات الواردة في مقالة إعداد طريقة دفع.

لكي يتمكّن التاجر من استخدام تطبيق الدفع، عليه استخدام Payment Request API و تحديد طريقة الدفع التي تتيحها باستخدام معرّف طريقة الدفع.

إذا كان لديك معرّف طريقة دفع فريد لتطبيق الدفع، يمكنك إعداد بيان طريقة الدفع الخاص بك ليتمكّن المتصفّح من اكتشاف تطبيقك.

الخطوة 2: إبلاغ التاجر إذا كان لدى العميل أداة دفع مسجّلة جاهزة للدفع

يمكن للتاجر استدعاء hasEnrolledInstrument() للاستعلام عمّا إذا كان بإمكان العميل إجراء دفعة. يمكنك تنفيذ IS_READY_TO_PAY كخدمة Android للإجابة عن هذا الاستعلام.

AndroidManifest.xml

عليك الإعلان عن خدمتك باستخدام intent filter يتضمّن الإجراء 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>

إنّ خدمة IS_READY_TO_PAY اختيارية. إذا لم يكن هناك معالج أهداف من هذا النوع في تطبيق الدفع، يفترض متصفّح الويب أنّ التطبيق يمكنه دائمًا إجراء الدفعات.

AIDL

يتم تعريف واجهة برمجة التطبيقات لخدمة IS_READY_TO_PAY في AIDL. عليك إنشاء ملفَّين بتنسيق AIDL يتضمّنان المحتوى التالي:

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

تنفيذ IsReadyToPayService

يظهر أبسط تنفيذ لـ IsReadyToPayService في المثال التالي:

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

جافا

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

الردّ

يمكن للخدمة إرسال ردّها باستخدام الطريقة handleIsReadyToPay(Boolean).

Kotlin

callback?.handleIsReadyToPay(true)

جافا

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

الإذن

يمكنك استخدام Binder.getCallingUid() للتحقّق من هوية البرنامج الذي يطلب الزحف. يُرجى العِلم أنّه عليك إجراء ذلك في الطريقة isReadyToPay وليس في الطريقة onBind، لأنّ نظام التشغيل Android يمكنه تخزين اتصال الخدمة مؤقتًا وإعادة استخدامه، ما لا يؤدي إلى تشغيل الطريقة onBind().

Kotlin

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());
        // ...

عليك دائمًا التحقّق من مَعلمات الإدخال بحثًا عن null عند تلقّي طلبات الاتصال بين العمليات (IPC). ويُعدّ ذلك مهمًا بشكل خاص لأنّ الإصدارات المختلفة أو المتفرّعة من نظام التشغيل Android يمكن أن تتصرف بطرق غير متوقّعة وتؤدي إلى حدوث أخطاء إذا لم يتم التعامل معها.

في حين أنّ packageManager.getPackagesForUid() يعرض عادةً عنصرًا واحدًا، يجب أن يتعامل الرمز البرمجي مع السيناريو غير الشائع الذي يستخدم فيه البرنامج الذي يطلب الزحف أسماء حزم متعدّدة. يضمن ذلك بقاء تطبيقك قويًا.

اطّلِع على مقالة التحقّق من شهادة التوقيع الخاصة بالبرنامج الذي يطلب الزحف لمعرفة كيفية التحقّق من أنّ الحزمة التي تطلب الزحف تتضمّن التوقيع الصحيح.

المعلمات

تمت إضافة parameters Bundle في Chrome 139. يجب دائمًا التحقّق منه بحثًا عن null.

يتم تمرير المَعلمات التالية إلى الخدمة في parameters Bundle:

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

تمت إضافة packageName في Chrome 138. عليك التحقّق من هذه المَعلمة مقابل Binder.getCallingUid() قبل استخدام قيمتها. هذا التحقّق ضروري لأنّ حزمة parameters تخضع للتحكّم الكامل من قِبل البرنامج الذي يطلب الزحف، بينما يتحكّم نظام التشغيل Android في Binder.getCallingUid().

تكون قيمة topLevelCertificateChain هي null في WebView وعلى المواقع الإلكترونية غير المستندة إلى HTTPS التي تُستخدم عادةً للاختبار المحلي، مثل http://localhost.

الخطوة 3: السماح للعميل بإجراء الدفع

يستدعي التاجر show() لـ تشغيل تطبيق الدفع ليتمكّن العميل من إجراء دفعة. يتم استدعاء تطبيق الدفع باستخدام هدف Android PAY مع معلومات المعاملة في مَعلمات الهدف.

يردّ تطبيق الدفع باستخدام methodName وdetails، وهما خاصّتان بتطبيق الدفع وغير مرئيتَين للمتصفّح. يحوّل المتصفّح السلسلة details إلى قاموس JavaScript للتاجر باستخدام إلغاء تسلسل سلسلة JSON، ولكنّه لا يفرض أي صلاحية بخلاف ذلك. لا يعدّل المتصفّح details؛ ويتم تمرير قيمة هذه المَعلمة مباشرةً إلى التاجر.

AndroidManifest.xml

يجب أن يحتوي النشاط الذي يتضمّن فلتر الأهداف PAY على علامة <meta-data> تحدّد معرّف طريقة الدفع التلقائية للتطبيق.

لإتاحة طرق دفع متعدّدة، أضِف علامة <meta-data> باستخدام مورد <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 قائمة سلاسل، يجب أن يكون كل منها عنوان URL مطلقًا صالحًا يتضمّن نظام HTTPS كما هو موضّح هنا.

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

المعلمات

يتم تمرير المَعلمات التالية إلى النشاط كإضافات Intent:

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

Kotlin

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

جافا

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

methodNames

أسماء الطرق المستخدَمة. العناصر هي المفاتيح في قاموس methodData. هذه هي الطرق التي يتيحها تطبيق الدفع.

Kotlin

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

جافا

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

methodData

ربط كل من methodNames بـ methodData.

Kotlin

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

جافا

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

merchantName

محتوى علامة HTML <title> لصفحة الدفع الخاصة بالتاجر (سياق التصفّح الأعلى مستوى في المتصفّح)

Kotlin

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

جافا

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

topLevelOrigin

مصدر التاجر بدون النظام (المصدر بدون النظام لسياق التصفّح الأعلى مستوى) على سبيل المثال، يتم تمرير https://mystore.com/checkout على النحو mystore.com.

Kotlin

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

جافا

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

topLevelCertificateChain

سلسلة شهادات التاجر (سلسلة شهادات سياق التصفّح الأعلى مستوى) تكون القيمة null لـ WebView أو localhost أو ملف على القرص. كل Parcelable هو حزمة تتضمّن مفتاح certificate وقيمة عبارة عن مصفوفة بايت.

Kotlin

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

المصدر بدون النظام لسياق التصفّح في iframe الذي استدعى الدالة الإنشائية new PaymentRequest(methodData, details, options) في JavaScript إذا تم استدعاء الدالة الإنشائية من سياق أعلى مستوى، تكون قيمة هذه المَعلمة مساوية لقيمة المَعلمة topLevelOrigin.

Kotlin

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

جافا

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

total

سلسلة JSON تمثّل المبلغ الإجمالي للمعاملة

Kotlin

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

جافا

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

في ما يلي مثال على محتوى السلسلة:

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

modifiers

ناتج JSON.stringify(details.modifiers)، حيث لا يحتوي details.modifiers إلا على supportedMethods وdata وtotal

paymentRequestId

حقل PaymentRequest.id الذي يجب أن تربطه تطبيقات "الدفع الفوري" بحالة المعاملة ستستخدم المواقع الإلكترونية الخاصة بالتجار هذا الحقل للاستعلام عن تطبيقات "الدفع الفوري" لمعرفة حالة المعاملة خارج النطاق.

Kotlin

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

جافا

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

الردّ

يمكن للنشاط إرسال ردّه مرة أخرى من خلال setResult مع RESULT_OK.

Kotlin

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("details", "{\"token\": \"put-some-data-here\"}");
result.putExtras(extras);
setResult(Activity.RESULT_OK, result);
finish();

عليك تحديد مَعلمتَين كإضافات Intent:

  • methodName: اسم الطريقة المستخدَمة
  • details: سلسلة JSON تحتوي على المعلومات اللازمة للتاجر لإكمال المعاملة إذا كانت success هي true، يجب إنشاء details بطريقة تنجح فيها JSON.parse(details). إذا لم تكن هناك بيانات يجب إرجاعها، يمكن أن تكون هذه السلسلة "{}"، والتي سيتلقّاها الموقع الإلكتروني للتاجر كقاموس JavaScript فارغ.

يمكنك تمرير RESULT_CANCELED إذا ألغى المستخدم المعاملة في تطبيق الدفع. سيؤدي ذلك إلى رفض request.show() مع AbortError على الموقع الإلكتروني للتاجر، ما يشير إلى إلغاء المستخدم.

Kotlin

setResult(Activity.RESULT_CANCELED)
finish()

جافا

setResult(Activity.RESULT_CANCELED);
finish();

اعتبارًا من Chrome 149، يتم إتاحة قيم النتائج التالية:

Kotlin

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

جافا

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

إذا تعذّر تشغيل تطبيق الدفع بسبب خطأ داخلي، يمكنك الإشارة إلى ذلك من خلال تمرير Activity.RESULT_FIRST_USER كرمز نتيجة.

إذا تم إرجاع INTERNAL_PAYMENT_APP_ERROR، سيتم رفض request.show() مع OperationError على الموقع الإلكتروني للتاجر، ما يشير إلى حدوث خطأ في تطبيق الدفع.

يسمح هذا التمييز بين RESULT_CANCELED (0) للإلغاء من قِبل المستخدم، ما يؤدي إلى حدوث AbortError، وINTERNAL_PAYMENT_APP_ERROR (1) لحدوث خطأ داخلي في التطبيق، ما يؤدي إلى حدوث OperationError، للتجار بإنشاء مسارات أفضل للمستخدمين.

Kotlin

setResult(Activity.RESULT_FIRST_USER)
finish()

جافا

setResult(Activity.RESULT_FIRST_USER);
finish();

إذا تم ضبط نتيجة النشاط لردّ الدفع الذي تم تلقّيه من تطبيق الدفع الذي تم استدعاؤه على RESULT_OK، سيتحقّق Chrome من عدم احتواء الإضافات على methodName وdetails غير فارغتَين. إذا تعذّر التحقّق من الصحة، سيعرض Chrome وعدًا مرفوضًا من request.show() مع إحدى رسائل الخطأ التالية التي تظهر للمطوّرين:

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

الإذن

يمكن للنشاط التحقّق من البرنامج الذي يطلب الزحف باستخدام الطريقة getCallingPackage().

Kotlin

val caller: String? = callingPackage

جافا

String caller = getCallingPackage();

الخطوة الأخيرة هي التحقّق من شهادة التوقيع الخاصة بالبرنامج الذي يطلب الزحف للتأكّد من أنّ الحزمة التي تطلب الزحف تتضمّن التوقيع الصحيح.

الخطوة 4: التحقّق من شهادة التوقيع الخاصة بالبرنامج الذي يطلب الزحف

يمكنك التحقّق من اسم حزمة البرنامج الذي يطلب الزحف باستخدام Binder.getCallingUid() في IS_READY_TO_PAY، وباستخدام Activity.getCallingPackage() في PAY. للتحقّق فعليًا من أنّ البرنامج الذي يطلب الزحف هو المتصفّح الذي تقصده، عليك التحقّق من شهادة التوقيع والتأكّد من أنّها تتطابق مع القيمة الصحيحة.

إذا كنت تستهدف مستوى واجهة برمجة التطبيقات 28 والإصدارات الأحدث وتدمج مع متصفّح يتضمّن شهادة توقيع واحدة، يمكنك استخدام 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
)

جافا

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() للمتصفّحات التي تتضمّن شهادة واحدة، لأنّها تتعامل بشكل صحيح مع تدوير الشهادات. (يتضمّن Chrome شهادة توقيع واحدة.) لا يمكن للتطبيقات التي تتضمّن شهادات توقيع متعدّدة تدويرها.

إذا كنت بحاجة إلى إتاحة مستوى واجهة برمجة التطبيقات 27 والإصدارات الأقدم، أو إذا كنت بحاجة إلى التعامل مع المتصفّحات التي تتضمّن شهادات توقيع متعدّدة، يمكنك استخدام 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)

جافا

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

تصحيح الأخطاء

استخدِم الأمر التالي لمراقبة الأخطاء أو الرسائل المعلوماتية:

adb logcat | grep -i pay