تعرَّف على كيفية تكييف تطبيق الدفع على Android للعمل مع Web Payments وتوفير تجربة مستخدم أفضل للعملاء.
توفّر واجهة برمجة التطبيقات Payment Request API واجهة برمجة تطبيقات مدمجة مستندة إلى المتصفّح على الويب تتيح للمستخدمين إدخال معلومات الدفع المطلوبة بسهولة أكبر من أي وقت مضى. يمكن أيضًا لواجهة برمجة التطبيقات استدعاء التطبيقات المتعلّقة بالدفع المخصّصة للمنصة.
مقارنةً باستخدام "نوايا Android" فقط، تتيح "عمليات الدفع على الويب" عملية دمج أفضل مع المتصفّح والأمان وتجربة المستخدم:
- تم إطلاق تطبيق الدفع كنموذج في سياق الموقع الإلكتروني للتاجر.
- تُعدّ عملية التنفيذ عنصرًا تكميليًا لتطبيق الدفع الحالي، ما يتيح لك الاستفادة من قاعدة مستخدمي تطبيقك.
- يتم التحقّق من توقيع تطبيق الدفع لمنع التثبيت من مصدر غير معروف.
- يمكن أن تتيح تطبيقات الدفع طرق دفع متعددة.
- يمكن دمج أي طريقة دفع، مثل العملات المشفَّرة والتحويلات المصرفية وغير ذلك. يمكن لتطبيقات الدفع على أجهزة Android أيضًا دمج طرق تتطلب الوصول إلى شريحة الجهاز على الجهاز.
تستغرق عملية تنفيذ Web Payments في تطبيق دفع على Android أربع خطوات:
- إتاحة تطبيق الدفع للتجّار
- إبلاغ التاجر إذا كان لدى العميل وسيلة دفع مسجّلة (مثل بطاقة الائتمان) جاهزة للدفع
- دع العميل يدفع.
- التحقّق من شهادة التوقيع الخاصة بالبرنامج الذي يطلب الزحف
للاطّلاع على Web Payments أثناء العمل، يمكنك الاطّلاع على الإصدار التمهيدي android-web-payment .
الخطوة 1: السماح للتجّار باكتشاف تطبيق الدفع الخاص بك
لكي يتمكّن التاجر من استخدام تطبيق الدفع، عليه استخدام 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
يحتويَين على المحتوى التالي:
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
intentPAY
في 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
سلسلة شهادات التاجر (سلسلة شهادات سياق التصفّح من المستوى الأعلى) قيمة فارغة لـ localhost والملف على القرص، وكلاهما سياقان آمنَان
بدون شهادات طبقة المقابس الآمنة كل 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()
يجب تحديد مَعلمتَين على أنّهما إضافات للهدف:
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 والإصدارات الأقدم، أو إذا كنت بحاجة إلى التعامل مع browsers التي تحتوي على شهادات توقيع متعددة، يمكنك استخدام 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) } }