دليل مطوّري تطبيقات الدفع المتوافقة مع Android

تعرَّف على كيفية تعديل تطبيق Android للدفع للعمل مع Web Payments وتقديم تجربة أفضل للعملاء.

يويتشي أراكي
يويتشي أراكي

تقدّم واجهة Payment Request API إلى الويب واجهة مضمّنة مستندة إلى المتصفّح تتيح للمستخدمين إدخال معلومات الدفع المطلوبة بشكل أسهل من أي وقت مضى. يمكن لواجهة برمجة التطبيقات أيضًا استدعاء تطبيقات دفع خاصة بنظام التشغيل.

التوافق مع المتصفح

  • 60
  • 15
  • 11.1

المصدر

مسار الدفع من خلال تطبيق Google Pay الخاص بالمنصة والذي يستخدم دفعات الويب.

بالمقارنة باستخدام أغراض Android فقط، تتيح "عمليات الدفع على الويب" إمكانية دمج أفضل مع المتصفّح والأمان وتجربة المستخدم:

  • تم إطلاق تطبيق الدفع في سياق موقع التاجر الإلكتروني.
  • يُعد التنفيذ مكملاً لتطبيق الدفع الحالي، ما يتيح لك الاستفادة من قاعدة المستخدمين.
  • يتم وضع علامة على توقيع تطبيق الدفع لمنع التثبيت من مصدر غير معروف.
  • يمكن أن تتيح تطبيقات الدفع استخدام طرق دفع متعدّدة.
  • ويمكن دمج أي طريقة دفع، مثل العملة المشفّرة والحوالات المصرفية وغيرها. يمكن لتطبيقات الدفع على أجهزة Android أيضًا دمج طُرق تتطلّب الوصول إلى شريحة الجهاز على الجهاز.

يستغرق تنفيذ "دفعات الويب" في تطبيق الدفع على Android أربع خطوات:

  1. يمكنك السماح للتجّار باكتشاف تطبيق الدفع.
  2. يجب إبلاغ التاجر بما إذا كان العميل لديه أداة مسجَّلة (مثل بطاقة الائتمان) جاهزة للدفع.
  3. السماح للعميل بإجراء الدفع
  4. تحقَّق من شهادة توقيع المتصل.

للاطلاع على طريقة الدفع عبر الويب، يمكنك الاطلاع على العرض التوضيحي ل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) } }