تعرَّف على كيفية تكييف تطبيق الدفع على Android للعمل مع Web Payments وتوفير تجربة مستخدم أفضل للعملاء.
تاريخ النشر: 5 أيار (مايو) 2020، تاريخ آخر تعديل: 27 أيار (مايو) 2025
توفّر واجهة برمجة التطبيقات Payment Request API واجهة برمجة تطبيقات مدمجة مستندة إلى المتصفّح على الويب تتيح للمستخدمين إدخال معلومات الدفع المطلوبة بسهولة أكبر من أي وقت مضى. يمكن أن تستدعي واجهة برمجة التطبيقات أيضًا تطبيقات دفع خاصة بالنظام الأساسي.
مقارنةً باستخدام "نوايا Android" فقط، تتيح "عمليات الدفع على الويب" عملية دمج أفضل مع المتصفّح والأمان وتجربة المستخدم:
- يتم تشغيل تطبيق الدفع كإطار مشروط في سياق الموقع الإلكتروني للتاجر.
- يُعدّ التنفيذ مكملاً لتطبيق الدفع الحالي، ما يتيح لك الاستفادة من قاعدة المستخدمين.
- يتم التحقّق من توقيع تطبيق الدفع لمنع التثبيت من مصدر غير معروف.
- يمكن أن تتيح تطبيقات الدفع طرق دفع متعددة.
- يمكن دمج أي طريقة دفع، مثل العملات المشفّرة والتحويلات المصرفية وغير ذلك. يمكن لتطبيقات الدفع على أجهزة Android دمج طرق تتطلب الوصول إلى شريحة الجهاز.
تستغرق عملية تنفيذ Web Payments في تطبيق دفع على Android أربع خطوات:
- إتاحة تطبيق الدفع للتجّار
- إبلاغ التاجر إذا كان لدى العميل أداة مسجّلة (مثل بطاقة الائتمان) جاهزة للدفع
- السماح للعميل بإجراء الدفع
- التحقّق من شهادة التوقيع الخاصة بالبرنامج الذي يطلب الزحف
للاطّلاع على Web Payments أثناء العمل، يمكنك الاطّلاع على الإصدار التمهيدي لتطبيق android-web-payment.
الخطوة 1: السماح للتجّار باكتشاف تطبيق الدفع
اضبط سمة related_applications
في بيان تطبيق الويب وفقًا
للتعليمات الواردة في مقالة إعداد طريقة دفع.
لكي يتمكّن التاجر من استخدام تطبيق الدفع، عليه استخدام Payment Request API وتحديد طريقة الدفع التي تتيحها باستخدام معرِّف طريقة الدفع.
إذا كان لديك معرّف طريقة دفع فريد لتطبيق الدفع، يمكنك إعداد بيان طريقة الدفع الخاص بك حتى تتمكّن المتصفّحات من اكتشاف تطبيقك.
الخطوة 2: إبلاغ التاجر إذا كان لدى العميل أداة مسجّلة جاهزة للدفع
يمكن للتاجر الاتصال برقم hasEnrolledInstrument()
للتحقّق مما إذا كان بإمكان العميل
إجراء دفعة. يمكنك
تنفيذ IS_READY_TO_PAY
كخدمة Android للإجابة عن هذا الطلب.
AndroidManifest.xml
حدِّد خدمتك باستخدام فلتر أهداف يتضمّن الإجراء
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
اختيارية. إذا لم يكن هناك معالِج نوايا مماثل في
تطبيق الدفع، يفترض متصفّح الويب أنّه يمكن للتطبيق إجراء
عمليات الدفع في أي وقت.
لغة تعريف واجهة نظام Android (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
في المثال التالي:
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)
.
callback?.handleIsReadyToPay(true)
if (callback != null) {
callback.handleIsReadyToPay(true);
}
الإذن
يمكنك استخدام Binder.getCallingUid()
لمعرفة هوية المتصل. يُرجى العِلم أنّه
عليك إجراء ذلك في طريقة isReadyToPay
وليس في طريقة onBind
،
لأنّ نظام التشغيل Android يمكنه تخزين اتصال الخدمة مؤقتًا وإعادة استخدامه، ما يؤدي إلى عدم
تفعيل طريقة 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());
// ...
تحقَّق دائمًا من مَعلمات الإدخال الخاصة بـ null
عند تلقّي مكالمات
العملية التواصل بين العمليات (IPC). وهذا مهم بشكلٍ خاص لأنّ الإصدارات المختلفة أو الإصدارات المشتقة من نظام التشغيل Android قد تتصرف بطرق غير متوقّعة وقد تؤدي إلى حدوث أخطاء في حال عدم معالجتها.
على الرغم من أنّ packageManager.getPackagesForUid()
عادةً ما
يعرض عنصرًا واحدًا، يجب أن يتعامل الرمز البرمجي مع السيناريو غير الشائع الذي يستخدم فيه المُتصل عدة
أسماء حِزم. ويضمن ذلك بقاء تطبيقك
متينًا.
اطّلِع على مقالة التحقّق من شهادة التوقيع للمتصل لمعرفة كيفية التحقّق من أنّ حزمة الاتصال تحتوي على التوقيع الصحيح.
المعلمات
تمت إضافة حِزمة parameters
في الإصدار 139 من Chrome. يجب التحقّق دائمًا مما إذا كان null
.
يتم تمرير المَعلمات التالية إلى الخدمة في حِزمة parameters
:
packageName
methodNames
methodData
topLevelOrigin
paymentRequestOrigin
topLevelCertificateChain
تمت إضافة الرمز packageName
في الإصدار 138 من Chrome. يجب التحقّق من
هذه المَعلمة مقابل Binder.getCallingUid()
قبل
استخدام قيمتها. هذه العملية ضرورية لأنّه
تخضع حزمة parameters
للتحكّم الكامل من قِبل المتصل،
بينما يتحكّم نظام التشغيل Android في حزمة Binder.getCallingUid()
.
يكون topLevelCertificateChain
هو null
في WebView وعلى المواقع الإلكترونية التي لا تستخدم بروتوكول https
والتي يتم استخدامها عادةً للاختبار على الجهاز، مثل http://localhost
.
الخطوة 3: السماح للعميل بإجراء الدفع
يتصل التاجر بـ show()
لتشغيل
تطبيق الدفع
حتى يتمكّن العميل من إجراء عملية دفع. يتمّ استدعاء تطبيق الدفع باستخدام PAY
هدف Android
مع معلومات المعاملة في مَعلمات الهدف.
يستجيب تطبيق الدفع بالرمزَين 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
val extras: Bundle? = getIntent()?.extras
Bundle extras = getIntent() != null ? getIntent().getExtras() : null;
methodNames
أسماء الطرق المستخدَمة العناصر هي مفاتيح القاموس
methodData
. هذه هي الطرق التي يتيح تطبيق الدفع استخدامها.
val methodNames: List<String>? = extras.getStringArrayList("methodNames")
List<String> methodNames = extras.getStringArrayList("methodNames");
methodData
تعيين من كلّ من methodNames
إلى
methodData
val methodData: Bundle? = extras.getBundle("methodData")
Bundle methodData = extras.getBundle("methodData");
merchantName
محتوى علامة HTML <title>
لصفحة الدفع الخاصة بالتاجر (سياق التصفّح من المستوى الأعلى للمتصفّح).
val merchantName: String? = extras.getString("merchantName")
String merchantName = extras.getString("merchantName");
topLevelOrigin
مصدر التاجر بدون المخطّط (مصدر
سياق التصفّح من المستوى الأعلى بدون المخطّط) على سبيل المثال، يتم
تمرير https://mystore.com/checkout
على أنّه mystore.com
.
val topLevelOrigin: String? = extras.getString("topLevelOrigin")
String topLevelOrigin = extras.getString("topLevelOrigin");
topLevelCertificateChain
سلسلة شهادات التاجر (سلسلة شهادات سياق التصفّح من المستوى الأعلى) تكون القيمة null
لمكوّن WebView أو المضيف المحلي أو ملف على القرص.
كل Parcelable
هو حِزمة تحتوي على مفتاح certificate
وقيمة صفيف بايت.
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
من سياق المستوى الأعلى، تكون قيمة المَعلمة topLevelOrigin
مساوية لقيمة المَعلمة topLevelOrigin
.
val paymentRequestOrigin: String? = extras.getString("paymentRequestOrigin")
String paymentRequestOrigin = extras.getString("paymentRequestOrigin");
total
سلسلة JSON التي تمثّل إجمالي مبلغ المعاملة
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
الذي يجب أن تربطه تطبيقات "الدفع الفوري" بحالة
المعاملة ستستخدم المواقع الإلكترونية للتجّار هذا الحقل لطلب معلومات من
تطبيقات "الدفع الفوري" عن حالة المعاملة خارج النطاق.
val paymentRequestId: String? = extras.getString("paymentRequestId")
String paymentRequestId = extras.getString("paymentRequestId");
الردّ
يمكن للنشاط إرسال رده من خلال setResult
باستخدام 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("details", "{\"token\": \"put-some-data-here\"}");
result.putExtras(extras);
setResult(Activity.RESULT_OK, result);
finish();
يجب تحديد مَعلمتَين كإضافات للهدف:
methodName
: اسم الطريقة المستخدَمة-
details
: سلسلة JSON تحتوي على المعلومات اللازمة للتاجر لإتمام المعاملة إذا كان النجاح هوtrue
، يجب إنشاءdetails
بطريقة تؤدي إلى نجاحJSON.parse(details)
. إذا لم يكن هناك أي بيانات يجب عرضها، يمكن أن تكون هذه السلسلة"{}"
، والتي سيتلقّاها الموقع الإلكتروني للتاجر كقائمة فارغة بلغة JavaScript.
يمكنك تمرير القيمة RESULT_CANCELED
إذا لم تكتمل المعاملة في
تطبيق الدفع، على سبيل المثال، إذا تعذّر على المستخدم كتابة رمز التعريف الشخصي الصحيح
لحسابه في تطبيق الدفع. قد يسمح المتصفّح للمستخدم باختيار
تطبيق دفع مختلف.
setResult(Activity.RESULT_CANCELED)
finish()
setResult(Activity.RESULT_CANCELED);
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()
.
val caller: String? = callingPackage
String caller = getCallingPackage();
الخطوة الأخيرة هي التحقّق من شهادة التوقيع للمتصل لتأكيد أنّ حزمة الاتصال تحتوي على التوقيع الصحيح.
الخطوة 4: التحقّق من شهادة التوقيع الخاصة بالمتصل
يمكنك التحقّق من اسم حزمة المتصل باستخدام Binder.getCallingUid()
في
IS_READY_TO_PAY
، وActivity.getCallingPackage()
في PAY
. للتأكّد من أنّ المُرسِل هو المتصفّح الذي تريده، عليك التحقّق من شهادة التوقيع والتأكّد من أنّها تتطابق مع القيمة الصحيحة.
إذا كنت تستهدِف المستوى 28 من واجهة برمجة التطبيقات والإصدارات الأحدث وكنت تُجري عملية دمج مع متصفّح
يتضمّن شهادة توقيع واحدة، يمكنك استخدام
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()
لمتصفّحات الشهادات الفردية، لأنّها تتعامل بشكل صحيح مع عملية تدوير الشهادات. (يحتوي Chrome على
شهادة توقيع واحدة). لا يمكن للتطبيقات التي تحتوي على شهادات توقيع متعددة
تغييرها.
إذا كنت بحاجة إلى إتاحة الإصدار 27 من واجهة برمجة التطبيقات والإصدارات الأقدم، أو إذا كنت بحاجة إلى التعامل مع browsers التي تستخدم عدة شهادات توقيع، يمكنك استخدام 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);
تصحيح الأخطاء
استخدِم الأمر التالي لرصد الأخطاء أو الرسائل الإجرائية:
adb logcat | grep -i pay