تعرَّف على كيفية تعديل تطبيق الدفع على Android بما يتوافق مع Web Payments وتقديم تجربة مستخدم أفضل للعملاء.
توفّر Payment Request API للويب واجهة مدمجة مستندة إلى المتصفّح تتيح للمستخدمين إدخال معلومات الدفع المطلوبة بشكل أسهل من أي وقت مضى. يمكن لواجهة برمجة التطبيقات أيضًا استدعاء تطبيقات دفع خاصة بالنظام الأساسي.
مقارنةً باستخدام أهداف Android Intent فقط، تتيح خدمة Web Payments التكامل بشكل أفضل مع المتصفّح والأمان وتجربة المستخدم:
- تم إطلاق تطبيق الدفع كوسيلة في سياق الموقع الإلكتروني للتاجر.
- تُعدّ عملية التنفيذ عنصرًا تكميليًا لتطبيق الدفع الحالي، ما يتيح لك الاستفادة من قاعدة مستخدمي تطبيقك.
- يتم وضع علامة في مربّع توقيع تطبيق الدفع لمنع التثبيت من مصدر غير معروف.
- يمكن أن تتيح تطبيقات الدفع استخدام طرق دفع متعددة.
- يمكن دمج أي طريقة دفع، مثل العملات المشفّرة والحوالات المصرفية وغيرها. يمكن لتطبيقات الدفع على أجهزة Android أيضًا دمج طرق تتطلب الوصول إلى شريحة الجهاز على الجهاز.
هناك أربع خطوات لاستخدام Web Payments في تطبيق الدفع على Android:
- يمكنك السماح للتجّار باكتشاف تطبيق الدفع الخاص بك.
- أخبِر التاجر بما إذا كان العميل لديه أداة مسجّلة (مثل بطاقة الائتمان) جاهزة للدفع.
- دع العميل يدفع.
- تحقَّق من شهادة توقيع المتصل.
للاطّلاع على طريقة عمل "دفعات الويب"، يمكنك الاطّلاع على العرض التوضيحي لميزة android-web-payment.
الخطوة 1: السماح للتجّار باكتشاف تطبيق الدفع الخاص بك
ليتمكّن التاجر من استخدام تطبيق الدفع الخاص بك، عليه استخدام واجهة برمجة التطبيقات "لطلب الدفع" وتحديد طريقة الدفع المتوافقة باستخدام معرّف طريقة الدفع.
إذا كان لديك معرّف طريقة دفع فريد لتطبيق الدفع الذي تستخدمه، يمكنك إعداد بيان طريقة الدفع حتى تتمكّن المتصفحات من اكتشاف تطبيقك.
الخطوة 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
بالمحتوى التالي:
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);
}
تنفيذ علامة IsReadyToPayService
يتم توضيح أبسط تنفيذ لـ IsReadyToPayService
في المثال التالي:
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
}
}
الإجابة
يمكن للخدمة إرسال ردها عبر طريقة handleIsReadyToPay(Boolean)
.
callback?.handleIsReadyToPay(true)
الإذن
يمكنك استخدام Binder.getCallingUid()
للتحقّق من هوية المتصل، مع العلم أنّ عليك إجراء ذلك بطريقة isReadyToPay
وليس بطريقة onBind
.
override fun isReadyToPay(callback: IsReadyToPayServiceCallback?) {
try {
val callingPackage = packageManager.getNameForUid(Binder.getCallingUid())
// …
راجع التحقق من شهادة توقيع المتصل لمعرفة كيفية التحقق من أن حزمة الاتصال تتضمن التوقيع الصحيح.
الخطوة 3: السماح للعميل بإجراء الدفع
يطلب التاجر من show()
تشغيل تطبيق الدفع
ليتمكّن العميل من إجراء عملية الدفع. يتم استدعاء تطبيق الدفع من خلال PAY
intent في Android مع تضمين معلومات المعاملة في معلمات intent.
يستجيب تطبيق الدفع باستخدام 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/method_names" />
</activity>
يجب أن تكون السمة resource
قائمة من السلاسل، ويجب أن يكون كل منها عنوان URL كاملاً وصالحًا مع مخطَّط HTTPS كما هو موضّح هنا.
<?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>
المعلمات
يتمّ تمرير المَعلمات التالية إلى النشاط كعناصر إضافية في Intent:
methodNames
methodData
topLevelOrigin
topLevelCertificateChain
paymentRequestOrigin
total
modifiers
paymentRequestId
val extras: Bundle? = intent?.extras
methodNames
تمثّل هذه السمة أسماء الطرق المُستخدَمة. العناصر هي المفاتيح في قاموس methodData
. في ما يلي الطرق المتوافقة مع تطبيق الدفع.
val methodNames: List<String>? = extras.getStringArrayList("methodNames")
methodData
تمثّل هذه السمة عملية ربط من methodNames
إلى
methodData
.
val methodData: Bundle? = extras.getBundle("methodData")
merchantName
محتوى علامة HTML <title>
الخاصة بصفحة الدفع الخاصة بالتاجر (سياق تصفّح المستوى الأعلى في المتصفّح)
val merchantName: String? = extras.getString("merchantName")
topLevelOrigin
أصل التاجر بدون المخطط (الأصل بدون مخطط لسياق التصفح ذي المستوى الأعلى). على سبيل المثال، يتم تمرير
https://mystore.com/checkout
كـ mystore.com
.
val topLevelOrigin: String? = extras.getString("topLevelOrigin")
topLevelCertificateChain
سلسلة شهادات التاجر (سلسلة الشهادات الخاصة بسياق التصفّح ذي المستوى الأعلى). قيمة فارغة للمضيف المحلي والملف على القرص، وكلاهما سياقان آمنان بدون شهادات طبقة المقابس الآمنة. كل Parcelable
عبارة عن حزمة تحتوي على مفتاح
certificate
وقيمة مصفوفة بايت.
val topLevelCertificateChain: Array<Parcelable>? =
extras.getParcelableArray("topLevelCertificateChain")
val list: List<ByteArray>? = topLevelCertificateChain?.mapNotNull { p ->
(p as Bundle).getByteArray("certificate")
}
paymentRequestOrigin
المصدر الخالي من المخطط لسياق تصفّح إطار iframe الذي استدعِ الدالة الإنشائية new
PaymentRequest(methodData, details, options)
في JavaScript. إذا تم استدعاء الدالة الإنشائية من سياق المستوى الأعلى، فإن قيمة هذه المعلمة تساوي قيمة المعلمة topLevelOrigin
.
val paymentRequestOrigin: String? = extras.getString("paymentRequestOrigin")
total
سلسلة JSON التي تمثّل المبلغ الإجمالي للمعاملة
val total: String? = extras.getString("total")
في ما يلي مثال على محتوى السلسلة:
{"currency":"USD","value":"25.00"}
modifiers
ناتج JSON.stringify(details.modifiers)
، حيث يحتوي details.modifiers
على supportedMethods
وtotal
فقط.
paymentRequestId
الحقل PaymentRequest.id
الذي يجب أن تربطه تطبيقات "الدفع الفوري"
بحالة المعاملة. ستستخدم المواقع الإلكترونية للتجّار هذا الحقل للاستعلام عن
تطبيقات "الدفع بدون تلامس الأجهزة" لمعرفة حالة المعاملة خارج النطاق.
val paymentRequestId: String? = 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:
methodName
: اسم الطريقة المستخدَمةdetails
: سلسلة JSON تحتوي على المعلومات اللازمة لكي يتمكّن التاجر من إكمال المعاملة إذا كانت قيمة النجاح هيtrue
، يجب إنشاءdetails
بطريقة تؤدي إلى نجاحJSON.parse(details)
.
يمكنك ضبط "RESULT_CANCELED
" إذا لم يتم إكمال المعاملة في
تطبيق الدفع، على سبيل المثال إذا تعذّر على المستخدم كتابة رقم التعريف الشخصي الصحيح
لحسابه في تطبيق الدفع. قد يسمح المتصفّح للمستخدم باختيار
تطبيق دفع مختلف.
setResult(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
الخطوة الأخيرة هي التحقق من شهادة توقيع المتصل للتأكد من أن حزمة الاتصال تتضمن التوقيع الصحيح.
الخطوة 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
)
ويفضَّل استخدام PackageManager.hasSigningCertificate()
في متصفِّحات الشهادات الفردية، لأنّه يتعامل مع تدوير الشهادات بشكل صحيح. (يستخدم Chrome
شهادة توقيع واحدة). لا يمكن للتطبيقات التي تحتوي على شهادات توقيع متعددة
تدويرها.
إذا كنت بحاجة إلى التوافق مع المستويات 27 والإصدارات الأقدم من واجهة برمجة التطبيقات، أو إذا كنت بحاجة إلى التعامل مع المتصفّحات التي تتضمن شهادات توقيع متعددة، يمكنك استخدام 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) } }