تعرَّف على كيفية تكييف تطبيق الدفع على Android للعمل مع "الدفع على الويب" وتوفير تجربة أفضل للمستخدمين.
تاريخ النشر: 5 مايو 2020، تاريخ آخر تعديل: 27 مايو 2025
توفّر Payment Request API على الويب واجهة مدمجة مستندة إلى المتصفّح تتيح للمستخدمين إدخال معلومات الدفع المطلوبة بسهولة لم يسبق لها مثيل. يمكن لواجهة برمجة التطبيقات أيضًا استدعاء تطبيقات الدفع الخاصة بالمنصة.
مقارنةً باستخدام Android Intents فقط، يتيح "الدفع على الويب" إمكانية التكامل بشكل أفضل مع المتصفّح والأمان وتجربة المستخدم:
- يتم تشغيل تطبيق الدفع كنافذة مشروطة في سياق الموقع الإلكتروني للتاجر.
- يُعدّ التنفيذ مكمّلاً لتطبيق الدفع الحالي، ما يتيح لك الاستفادة من قاعدة المستخدمين.
- يتم التحقّق من توقيع تطبيق الدفع لمنع التحميل الجانبي.
- يمكن أن تتيح تطبيقات الدفع طرق دفع متعدّدة.
- يمكن دمج أي طريقة دفع، مثل العملات المشفّرة والتحويلات المصرفية والمزيد. يمكن أن تدمج تطبيقات الدفع على أجهزة Android طرقًا تتطلّب الوصول إلى شريحة الأجهزة.
يتطلّب تنفيذ "الدفع على الويب" في تطبيق دفع على Android أربع خطوات:
- السماح للتجار باكتشاف تطبيق الدفع
- إبلاغ التاجر إذا كان لدى العميل أداة دفع مسجّلة (مثل بطاقة الائتمان) جاهزة للدفع
- السماح للعميل بإجراء الدفع
- التحقّق من شهادة التوقيع الخاصة بالبرنامج الذي يطلب الزحف
للاطّلاع على "الدفع على الويب" أثناء العمل، يمكنك الاطّلاع على العرض التوضيحي 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:
packageNamemethodNamesmethodDatatopLevelOriginpaymentRequestOrigintopLevelCertificateChain
تمت إضافة 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:
methodNamesmethodDatamerchantNametopLevelOrigintopLevelCertificateChainpaymentRequestOrigintotalmodifierspaymentRequestIdpaymentOptionsshippingOptions
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