تقديم معلومات الشحن والاتصال من تطبيق دفع على Android

كيفية تحديث تطبيق الدفع على Android لتوفير عنوان الشحن ومعلومات الاتصال الخاصة بالجهة المسدِّدة باستخدام واجهات برمجة التطبيقات Web Payments API

Sahel Sharify
Sahel Sharify

قد تكون إدخال عنوان الشحن ومعلومات الاتصال من خلال نموذج ويب تجربة مزعجة للعملاء. ويمكن أن يؤدي ذلك إلى حدوث أخطاء وانخفاض في نسبة الإحالات الناجحة.

لهذا السبب، تتيح واجهة برمجة التطبيقات Payment Request API ميزة لطلب عنوان الشحن ومعلومات الاتصال. ويعود ذلك بالفائدة على النحو التالي:

  • يمكن للمستخدمين اختيار العنوان الصحيح ببضع نقرات فقط.
  • ويتم عرض العنوان دائمًا بالتنسيق المعيار.
  • من غير المرجّح إرسال عنوان غير صحيح.

يمكن للمتصفّحات تأجيل جمع عنوان الشحن ومعلومات الاتصال إلى تطبيق دفع لتقديم تجربة دفع موحّدة. يُطلق على هذه الوظيفة اسم تفويض.

كلما أمكن، يفوّض Chrome جمع عنوان العميل للإرسال ومعلومات الاتصال به إلى تطبيق الدفع المتوافق مع Android الذي تمّ استدعاؤه. ويقلل التفوّض من الصعوبات التي تواجهك أثناء الدفع.

يمكن للموقع الإلكتروني للتاجر تعديل خيارات الشحن والسعر الإجمالي ديناميكيًا، وذلك استنادًا إلى اختيار العميل لعنوان الشحن وخيار الشحن.

تغيير خيار الشحن وعنوان الشحن في إجراء. اطّلِع على كيفية تأثير ذلك بشكل ديناميكي في خيارات الشحن والسعر الإجمالي.

لإضافة ميزة التفويض إلى تطبيق دفع حالي على Android، اتّبِع الخطوات التالية:

  1. الإفصاح عن عمليات التفويض المتوافقة
  2. تحليل PAY intent الإضافية لخيارات الدفع المطلوبة.
  3. تقديم المعلومات المطلوبة في ردّ الدفع
  4. [اختياري] إتاحة المسار الديناميكي:
    1. إبلاغ التاجر بالتغييرات في طريقة الدفع التي اختارها المستخدم أو عنوان الشحن أو خيار الشحن
    2. تلقّي تفاصيل دفع معدَّلة من التاجر (على سبيل المثال، المبلغ الإجمالي المعدَّل استنادًا إلى تكلفة خيار الشحن الذي تم اختياره)

الإفصاح عن التفويضات المتوافقة

يحتاج المتصفّح إلى معرفة قائمة المعلومات الإضافية التي يمكن أن يوفّرها تطبيق الدفع كي يتمكّن من تفويض جمع هذه المعلومات إلى تطبيقك. يُرجى توضيح التفويضات المتوافقة باعتبارها <meta-data> في ملف AndroidManifest.xml الخاص بتطبيقك.

<activity
  android:name=".PaymentActivity"
    <meta-data
    android:name="org.chromium.payment_supported_delegations"
    android:resource="@array/supported_delegations" />
</activity>

يجب أن تكون <resource> قائمة بسلاسل يتم اختيارها من القيم الصالحة التالية:

[ "payerName", "payerEmail", "payerPhone", "shippingAddress" ]

لا يمكن أن يقدّم المثال التالي سوى عنوان الشحن وعنوان البريد الإلكتروني للمدفوع عنه.

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <string-array name="supported_delegations">
    <item>payerEmail</item>
    <item>shippingAddress</item>
  </string-array>
</resources>

تحليل عناصر "PAY" الإضافية حسب النية بالشراء لخيارات الدفع المطلوبة

يمكن للتاجر تحديد معلومات مطلوبة إضافية باستخدام القاموس paymentOptions . سيقدّم Chrome قائمة بالخيارات المطلوبة التي يمكن لتطبيقك تقديمها من خلال تمرير المَعلمات التالية إلى نشاط PAY على أنّها إضافات Intent .

paymentOptions

paymentOptions هي المجموعة الفرعية لخيارات الدفع التي حدّدها التاجر والتي أعلن تطبيقك عن توفّر إمكانية تفويضها.

val paymentOptions: Bundle? = extras.getBundle("paymentOptions")
val requestPayerName: Boolean? = paymentOptions?.getBoolean("requestPayerName")
val requestPayerPhone: Boolean? = paymentOptions?.getBoolean("requestPayerPhone")
val requestPayerEmail: Boolean? = paymentOptions?.getBoolean("requestPayerEmail")
val requestShipping: Boolean? = paymentOptions?.getBoolean("requestShipping")
val shippingType: String? = paymentOptions?.getString("shippingType")

ويمكن أن يتضمّن المَعلمات التالية:

  • requestPayerName - القيمة المنطقية التي تشير إلى ما إذا كان اسم الدافع مطلوبًا أم لا.
  • requestPayerPhone: القيمة المنطقية التي تشير إلى ما إذا كان هاتف المدفوع عنه مطلوبًا أم لا.
  • requestPayerEmail: القيمة المنطقية التي تشير إلى ما إذا كان عنوان البريد الإلكتروني للدافع مطلوبًا أم لا
  • requestShipping: القيمة المنطقية التي تشير إلى ما إذا كانت معلومات الشحن مطلوبة أم لا
  • shippingType: السلسلة التي تعرض نوع الشحن يمكن أن يكون نوع الشحن هو "shipping" أو "delivery" أو "pickup". يمكن لتطبيقك استخدام هذا التلميح في واجهة المستخدم عند طلب عنوان المستخدم أو اختيار خيارات الشحن.

shippingOptions

تمثّل السمة shippingOptions مصفوفة من خيارات الشحن التي يحدّدها التاجر. لن تظهر هذه المَعلمة إلا عند paymentOptions.requestShipping == true.

val shippingOptions: List<ShippingOption>? =
    extras.getParcelableArray("shippingOptions")?.mapNotNull {
        p -> from(p as Bundle)
    }

كل خيار شحن هو Bundle يتضمّن المفاتيح التالية.

  • id: معرّف خيار الشحن
  • label: تصنيف خيار الشحن الذي يظهر للمستخدم
  • amount: حِزمة تكلفة الشحن التي تحتوي على مفتاحَي currency وvalue مع قيم سلاسل
    • تعرض السمة currency عملة تكلفة الشحن، وهي عبارة عن رمز أبجدي ISO4217 مكتوب بشكل صحيح مكوَّن من 3 أحرف.
    • value تعرِض قيمة تكلفة الشحن كقيمة مالية عشرية صالحة .
  • selected - ما إذا كان يجب اختيار خيار الشحن أم لا عندما يعرض تطبيق الدفع خيارات الشحن

تحتوي جميع المفاتيح باستثناء selected على قيم سلاسل. تتضمن selected قيمة منطقية.

val id: String = bundle.getString("id")
val label: String = bundle.getString("label")
val amount: Bundle = bundle.getBundle("amount")
val selected: Boolean = bundle.getBoolean("selected", false)

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

يجب أن يتضمّن تطبيقك المعلومات الإضافية المطلوبة في ردّه على نشاط PAY.

ولإجراء ذلك، يجب تحديد المَعلمات التالية على أنّها عناصر إضافية للهدف:

  • payerName - الاسم الكامل للدافع يجب أن تكون هذه السلسلة غير فارغة عندما تكون قيمة paymentOptions.requestPayerName صحيحة.
  • payerPhone: رقم هاتف المسؤول عن الدفع يجب أن تكون هذه السلسلة غير فارغة عندما تكون قيمة paymentOptions.requestPayerPhone صحيحة.
  • payerEmail: عنوان البريد الإلكتروني للمسؤول عن الدفع يجب أن تكون هذه القيمة سلسلة غير فارغة عندما تكون paymentOptions.requestPayerEmail صحيحة.
  • shippingAddress - عنوان الشحن الذي قدّمه المستخدم. يجب أن تكون هذه حزمة غير فارغة عندما تكون القيمة paymentOptions.requestShipping true. يجب أن تتضمّن الحزمة المفاتيح التالية التي تمثّل أجزاءً مختلفة في العنوان الجغرافي.
    • city
    • countryCode
    • dependentLocality
    • organization
    • phone
    • postalCode
    • recipient
    • region
    • sortingCode
    • addressLine تحتوي جميع المفاتيح غير addressLine على قيم سلسلة. addressLine هي صفيف من السلاسل.
  • shippingOptionId: معرّف خيار الشحن الذي اختاره المستخدم ويجب أن تكون هذه السلسلة غير فارغة عندما تكون القيمة paymentOptions.requestShipping صحيحة.

التحقّق من صحة الرد على طلب الدفع

إذا تم ضبط نتيجة النشاط الخاصة بنتيجة رد على دفع تم استلامها من تطبيق الدفع الذي تم استدعاؤه على RESULT_OK، سيتحقق Chrome من المعلومات الإضافية المطلوبة في الميزات الإضافية. إذا تعذّر إجراء عملية التحقّق، سيعرض Chrome وعدًا مرفوضًا من request.show() يتضمّن إحدى رسائل الخطأ التالية التي يواجهها المطوّرون:

'Payment app returned invalid response. Missing field "payerEmail".'
'Payment app returned invalid response. Missing field "payerName".'
'Payment app returned invalid response. Missing field "payerPhone".'
'Payment app returned invalid shipping address in response.'
'... is not a valid CLDR country code, should be 2 upper case letters [A-Z]'
'Payment app returned invalid response. Missing field "shipping option".'

نموذج التعليمات البرمجية التالي هو مثال على ردّ صالح:

fun Intent.populateRequestedPaymentOptions() {
    if (requestPayerName) {
        putExtra("payerName", "John Smith")
    }
    if (requestPayerPhone) {
        putExtra("payerPhone", "4169158200")
    }
    if (requestPayerEmail) {
        putExtra("payerEmail", "john.smith@gmail.com")
    }
    if(requestShipping) {
        val address: Bundle = Bundle()
        address.putString("countryCode", "CA")
        val addressLines: Array<String> =
                arrayOf<String>("111 Richmond st. West")
        address.putStringArray("addressLines", addressLines)
        address.putString("region", "Ontario")
        address.putString("city", "Toronto")
        address.putString("postalCode", "M5H2G4")
        address.putString("recipient", "John Smith")
        address.putString("phone", "4169158200")
        putExtra("shippingAddress", address)
        putExtra("shippingOptionId", "standard")
    }
}

اختياري: إتاحة مسار ديناميكي

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

لغة تعريف واجهة نظام Android ‏(AIDL)

لإشعار التاجر بالتغييرات الجديدة، استخدِم PaymentDetailsUpdateService الخدمة الموضّحة في ملف AndroidManifest.xml في Chrome. لاستخدام هذه الخدمة، أنشئ ملفَّين AIDL يتضمّنان المحتوى التالي:

app/src/main/aidl/org/chromium/components/payments/IPaymentDetailsUpdateService

package org.chromium.components.payments;
import android.os.Bundle;

interface IPaymentDetailsUpdateServiceCallback {
    oneway void updateWith(in Bundle updatedPaymentDetails);

    oneway void paymentDetailsNotUpdated();
}

app/src/main/aidl/org/chromium/components/payments/IPaymentDetailsUpdateServiceCallback

package org.chromium.components.payments;
import android.os.Bundle;
import org.chromium.components.payments.IPaymentDetailsUpdateServiceCallback;

interface IPaymentDetailsUpdateService {
    oneway void changePaymentMethod(in Bundle paymentHandlerMethodData,
            IPaymentDetailsUpdateServiceCallback callback);

    oneway void changeShippingOption(in String shippingOptionId,
            IPaymentDetailsUpdateServiceCallback callback);

    oneway void changeShippingAddress(in Bundle shippingAddress,
            IPaymentDetailsUpdateServiceCallback callback);
}

إبلاغ التاجر بالتغييرات في طريقة الدفع أو عنوان الشحن أو خيار الشحن الذي اختاره المستخدم

private fun bind() {
    // The action is introduced in Chrome version 92, which supports the service in Chrome
    // and other browsers (e.g., WebLayer).
    val newIntent = Intent("org.chromium.intent.action.UPDATE_PAYMENT_DETAILS")
        .setPackage(callingBrowserPackage)
    if (packageManager.resolveService(newIntent, PackageManager.GET_RESOLVED_FILTER) == null) {
        // Fallback to Chrome-only approach.
        newIntent.setClassName(
            callingBrowserPackage,
            "org.chromium.components.payments.PaymentDetailsUpdateService")
        newIntent.action = IPaymentDetailsUpdateService::class.java.name
    }
    isBound = bindService(newIntent, connection, Context.BIND_AUTO_CREATE)
}

private val connection = object : ServiceConnection {
    override fun onServiceConnected(className: ComponentName, service: IBinder) {
        val service = IPaymentDetailsUpdateService.Stub.asInterface(service)
        try {
            if (isOptionChange) {
                service?.changeShippingOption(selectedOptionId, callback)
            } else (isAddressChange) {
                service?.changeShippingAddress(selectedAddress, callback)
            } else {
                service?.changePaymentMethod(methodData, callback)
            }
        } catch (e: RemoteException) {
            // Handle the remote exception
        }
    }
}

يمكن أن يكون للمعلمة callingPackageName المستخدَمة في نية بدء الخدمة إحدى القيمة التالية استنادًا إلى المتصفّح الذي بدأ طلب الدفع .

قناة Chrome اسم الحزمة
إسطبل "com.android.chrome"
إصدار تجريبي "com.chrome.beta"
رامي "com.chrome.dev"
الكاناري "com.chrome.canary"
Chromium "org.chromium.chrome"
مربّع البحث السريع من Google (أداة تضمين WebLayer) "com.google.android.googlequicksearchbox"

changePaymentMethod

إرسال إشعار للتاجر بشأن التغييرات في طريقة الدفع التي اختارها المستخدم تحتوي حِزمة paymentHandlerMethodData على مفتاحَي methodName وdetails الاختياريَين، وكلاهما يتضمّن قيم سلاسل. سيبحث Chrome عن حزمة غير فارغة تحتوي على methodName غير فارغ، وسيرسل updatePaymentDetails يتضمّن إحدى رسائل الخطأ التالية عبر callback.updateWith في حال تعذّر إثبات الصحة.

'Method data required.'
'Method name required.'

changeShippingOption

إرسال إشعار للتاجر بشأن التغييرات في خيار الشحن الذي اختاره المستخدم يجب أن يكون shippingOptionId معرّفًا لأحد خيارات الشحن التي يحدّدها التاجر. سيبحث Chrome عن shippingOptionId غير فارغ ويُرسِل updatePaymentDetails يتضمّن رسالة الخطأ التالية عبر callback.updateWith في حال تعذّر إتمام عملية التحقّق.

'Shipping option identifier required.'

changeShippingAddress

يتم إشعار التاجر بالتغييرات في عنوان الشحن الذي قدّمه المستخدم. سيبحث Chrome عن حزمة shippingAddress غير فارغة تتضمّن قيمة countryCode صالحة وسيرسل updatePaymentDetails مع رسالة الخطأ التالية عبر callback.updateWith في حال تعذُّر التحقّق.

'Payment app returned invalid shipping address in response.'

رسالة خطأ الحالة غير الصالحة

إذا واجه Chrome حالة غير صالحة عند تلقّي أي من طلبات التغيير، سيتصل بخدمة callback.updateWith باستخدام حِزمة updatePaymentDetails محذوفة. لن تحتوي الحزمة إلا على مفتاح error مع "Invalid state". في ما يلي أمثلة على الحالات غير الصالحة:

  • عندما يظل Chrome في انتظار رد التاجر على تغيير سابق (مثلاً، حدث تغيير مستمر)
  • معرّف خيار الشحن المقدَّم من تطبيق الدفع لا ينتمي إلى أي من خيارات الشحن التي حدّدها التاجر.

تلقّي تفاصيل الدفع المعدَّلة من التاجر

private fun unbind() {
    if (isBound) {
        unbindService(connection)
        isBound = false
    }
}

private val callback: IPaymentDetailsUpdateServiceCallback =
    object : IPaymentDetailsUpdateServiceCallback.Stub() {
        override fun paymentDetailsNotUpdated() {
            // Payment request details have not changed.
            unbind()
        }

        override fun updateWith(updatedPaymentDetails: Bundle) {
            newPaymentDetails = updatedPaymentDetails
            unbind()
        }
    }

تتشابه السمة updatePaymentDetails مع قاموس PaymentRequestDetailsUpdate WebIDL (بعد إخفاء الحقل modifiers) وتتضمّن المفاتيح الاختيارية التالية:

  • total: حِزمة تحتوي على مفتاحَي currency وvalue، وكلا المفتاحَين لهما قيم سلاسل
  • shippingOptions - الصفيف القابل للتقسيم من خيارات الشحن
  • error: سلسلة تحتوي على رسالة خطأ عامة (على سبيل المثال، عندما لا يوفّر changeShippingOption معرّفًا صالحًا لخيار الشحن)
  • stringifiedPaymentMethodErrors - سلسلة JSON تمثّل أخطاء التحقّق لطريقة الدفع
  • addressErrors - حِزمة تحتوي على مفاتيح اختيارية مطابقة لقيم shipping address وسلسلة يمثّل كل مفتاح خطأ في عملية التحقّق من صحة القيمة المتعلّقة بالجزء المعني من عنوان الشحن.

ويعني عدم توفّر المفتاح أنّ قيمته لم تتغيّر.