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