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