מדריך למפתחים של אפליקציות תשלומים ל-Android

במאמר הזה מוסבר איך להתאים את אפליקציית התשלומים ל-Android כך שתפעל עם תשלומים באינטרנט ותספק ללקוחות חוויית משתמש טובה יותר.

פורסם: 5 במאי 2020, עודכן לאחרונה: 27 במאי 2025

Payment Request API (ממשק API לבקשות תשלום) מספק ממשק מובנה מבוסס-דפדפן באינטרנט, שמאפשר למשתמשים להזין את פרטי התשלום הנדרשים בקלות יותר מאי פעם. ממשק ה-API יכול גם להפעיל אפליקציות תשלום ספציפיות לפלטפורמה.

Browser Support

  • Chrome: 60.
  • Edge: 15.
  • Firefox: behind a flag.
  • Safari: 11.1.

Source

תהליך התשלום באמצעות אפליקציית Google Pay ספציפית לפלטפורמה שמשתמשת בתשלומים באינטרנט.

בהשוואה לשימוש רק ב-Android Intents, תשלומים באינטרנט מאפשרים שילוב טוב יותר עם הדפדפן, האבטחה וחוויית המשתמש:

  • אפליקציית התשלום נפתחת כחלון מודאלי בהקשר של אתר המוכר.
  • ההטמעה היא בנוסף לאפליקציית התשלומים הקיימת, ומאפשרת לכם לנצל את בסיס המשתמשים שלכם.
  • החתימה של אפליקציית התשלום נבדקת כדי למנוע התקנה ממקור לא ידוע.
  • אפליקציות תשלום יכולות לתמוך בכמה אמצעי תשלום.
  • אפשר לשלב כל אמצעי תשלום, כמו מטבעות קריפטוגרפיים, העברות בנקאיות ועוד. אפליקציות תשלום במכשירי Android יכולות אפילו לשלב שיטות שדורשות גישה לשבב החומרה במכשיר.

כדי להטמיע תשלומים באתר באפליקציית תשלומים ל-Android, צריך לבצע ארבעה שלבים:

  1. כך מוכרים יכולים לגלות את אפליקציית התשלום שלכם.
  2. העברת מידע למוכר אם ללקוח יש אמצעי תשלום רשום (כמו כרטיס אשראי) שאפשר לשלם איתו.
  3. אפשרות ללקוח לבצע תשלום.
  4. מאמתים את אישור החתימה של המתקשר.

כדי לראות את התכונה 'תשלומים באינטרנט' בפעולה, אפשר לעיין בהדגמה של android-web-payment.

שלב 1: מאפשרים למוֹכרים לגלות את אפליקציית התשלום שלכם

מגדירים את המאפיין related_applications בקובץ המניפסט של אפליקציית האינטרנט בהתאם להוראות שבמאמר הגדרת אמצעי תשלום.

כדי שמוֹכרים יוכלו להשתמש באפליקציית התשלומים שלכם, הם צריכים להשתמש ב-Payment Request API ולציין את אמצעי התשלום שאתם תומכים בו באמצעות מזהה אמצעי התשלום.

אם יש לכם מזהה אמצעי תשלום ייחודי לאפליקציית התשלום שלכם, אתם יכולים להגדיר מניפסט של אמצעי התשלום כדי שהדפדפנים יוכלו לגלות את האפליקציה שלכם.

שלב 2: מודיעים למוֹכר אם ללקוח יש אמצעי תשלום רשום שמוכן לתשלום

המוֹכר יכול להתקשר למספר hasEnrolledInstrument() כדי לבדוק אם הלקוח יכול לבצע תשלום. אפשר להטמיע את IS_READY_TO_PAY כשירות Android כדי לענות על השאילתה הזו.

AndroidManifest.xml

מצהירים על השירות באמצעות מסנן Intent עם הפעולה 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 הוא אופציונלי. אם אין מטפל כוונות כזה באפליקציית התשלום, דפדפן האינטרנט מניח שהאפליקציה תמיד יכולה לבצע תשלומים.

AIDL

ה-API של שירות IS_READY_TO_PAY מוגדר ב-AIDL. יוצרים שני קובצי AIDL עם התוכן הבא:

org/chromium/IsReadyToPayServiceCallback.aidl

package org.chromium;

interface IsReadyToPayServiceCallback {
    oneway void handleIsReadyToPay(boolean isReadyToPay);
}

org/chromium/IsReadyToPayService.aidl

package org.chromium;

import org.chromium.IsReadyToPayServiceCallback;

interface IsReadyToPayService {
    oneway void isReadyToPay(IsReadyToPayServiceCallback callback, in Bundle parameters);
}

הטמעה של IsReadyToPayService

בדוגמה הבאה מוצגת ההטמעה הפשוטה ביותר של IsReadyToPayService:

Kotlin

class SampleIsReadyToPayService : Service() {
    private val binder = object : IsReadyToPayService.Stub() {
        override fun isReadyToPay(callback: IsReadyToPayServiceCallback?, parameters: Bundle?) {
            callback?.handleIsReadyToPay(true)
        }
    }

    override fun onBind(intent: Intent?): IBinder? {
        return binder
    }
}

Java

import org.chromium.IsReadyToPayService;

public class SampleIsReadyToPayService extends Service {
    private final IsReadyToPayService.Stub mBinder =
        new IsReadyToPayService.Stub() {
            @Override
            public void isReadyToPay(IsReadyToPayServiceCallback callback, Bundle parameters) {
                if (callback != null) {
                    callback.handleIsReadyToPay(true);
                }
            }
        };

    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }
}

תשובה

השירות יכול לשלוח את התגובה שלו באמצעות השיטה handleIsReadyToPay(Boolean).

Kotlin

callback?.handleIsReadyToPay(true)

Java

if (callback != null) {
    callback.handleIsReadyToPay(true);
}

הרשאה

אפשר להשתמש ב-Binder.getCallingUid() כדי לבדוק מי המתקשר. שימו לב שצריך לעשות את זה בשיטה isReadyToPay ולא בשיטה onBind, כי מערכת ההפעלה Android יכולה לשמור במטמון את חיבור השירות ולעשות בו שימוש חוזר, מה שלא מפעיל את השיטה onBind().

Kotlin

override fun isReadyToPay(callback: IsReadyToPayServiceCallback?, parameters: Bundle?) {
    try {
        val untrustedPackageName = parameters?.getString("packageName")
        val actualPackageNames = packageManager.getPackagesForUid(Binder.getCallingUid())
        // ...

Java

@Override
public void isReadyToPay(IsReadyToPayServiceCallback callback, Bundle parameters) {
    try {
        String untrustedPackageName = parameters != null
                ? parameters.getString("packageName")
                : null;
        String[] actualPackageNames = packageManager.getPackagesForUid(Binder.getCallingUid());
        // ...

תמיד בודקים את פרמטרי הקלט של null כשמקבלים קריאות של תקשורת בין תהליכים (IPC). זה חשוב במיוחד כי גרסאות שונות או הסתעפויות של Android OS עלולות להתנהג באופן לא צפוי ולגרום לשגיאות אם לא מטפלים בהן.

בדרך כלל, packageManager.getPackagesForUid() מחזירה רכיב יחיד, אבל הקוד שלכם צריך לטפל בתרחיש הלא נפוץ שבו מתקשר משתמש בכמה שמות חבילות. כך תוכלו לוודא שהאפליקציה שלכם תישאר יציבה.

במאמר אימות אישור החתימה של המתקשר מוסבר איך לוודא שלחבילת ההתקשרות יש את החתימה הנכונה.

פרמטרים

parameters Bundle נוסף ב-Chrome 139. תמיד צריך לבדוק את זה מול null.

הפרמטרים הבאים מועברים לשירות ב-parameters Bundle:

  • packageName
  • methodNames
  • methodData
  • topLevelOrigin
  • paymentRequestOrigin
  • topLevelCertificateChain

הסמל packageName נוסף ב-Chrome 138. לפני שמשתמשים בערך של הפרמטר הזה, צריך לאמת אותו מול Binder.getCallingUid(). האימות הזה חיוני כי חבילת parameters נמצאת בשליטה מלאה של המתקשר, בעוד ש-Binder.getCallingUid() נמצאת בשליטה של מערכת ההפעלה Android.

הערך topLevelCertificateChain הוא null ב-WebView ובאתרים שאינם https, שמשמשים בדרך כלל לבדיקות מקומיות, כמו http://localhost.

שלב 3: מאפשרים ללקוח לבצע תשלום

המוֹכר מתקשר אל show() כדי להפעיל את אפליקציית התשלום כדי שהלקוח יוכל לשלם. הפעלת אפליקציית התשלום מתבצעת באמצעות intent של Android‏ PAY עם פרטי הטרנזקציה בפרמטרים של ה-intent.

אפליקציית התשלומים מגיבה עם methodName ו-details, שהם ספציפיים לאפליקציית התשלומים ולא שקופים לדפדפן. הדפדפן ממיר את המחרוזת details למילון JavaScript עבור המוכר באמצעות ביטול הסדר של מחרוזת JSON, אבל לא מבצע אימות מעבר לכך. הדפדפן לא משנה את details, והערך של הפרמטר הזה מועבר ישירות אל המוכר.

AndroidManifest.xml

לפעילות עם מסנן ה-Intent‏ 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/chromium_payment_method_names" />
</activity>

המאפיין android:resource צריך להיות רשימה של מחרוזות, שכל אחת מהן צריכה להיות כתובת URL תקינה וכתובת URL אבסולוטית עם סכימת HTTPS, כמו שמוצג כאן.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="chromium_payment_method_names">
        <item>https://alicepay.com/put/optional/path/here</item>
        <item>https://charliepay.com/put/optional/path/here</item>
    </string-array>
</resources>

פרמטרים

הפרמטרים הבאים מועברים לפעילות כIntent extras:

  • methodNames
  • methodData
  • merchantName
  • topLevelOrigin
  • topLevelCertificateChain
  • paymentRequestOrigin
  • total
  • modifiers
  • paymentRequestId
  • paymentOptions
  • shippingOptions

Kotlin

val extras: Bundle? = getIntent()?.extras

Java

Bundle extras = getIntent() != null ? getIntent().getExtras() : null;

methodNames

השמות של השיטות שבהן נעשה שימוש. הרכיבים הם המפתחות במילון methodData. אלה האמצעים שאפליקציית התשלום תומכת בהם.

Kotlin

val methodNames: List<String>? = extras.getStringArrayList("methodNames")

Java

List<String> methodNames = extras.getStringArrayList("methodNames");

methodData

מיפוי מכל אחד מהערכים methodNames אל methodData.

Kotlin

val methodData: Bundle? = extras.getBundle("methodData")

Java

Bundle methodData = extras.getBundle("methodData");

merchantName

התוכן של תג ה-HTML‏ <title> בדף התשלום של המוֹכר (הקשר הגלישה ברמה העליונה של הדפדפן).

Kotlin

val merchantName: String? = extras.getString("merchantName")

Java

String merchantName = extras.getString("merchantName");

topLevelOrigin

המקור של המוֹכר ללא הסכימה (המקור ללא הסכימה של הקשר הגלישה ברמה העליונה). לדוגמה, הערך https://mystore.com/checkout מועבר כ-mystore.com.

Kotlin

val topLevelOrigin: String? = extras.getString("topLevelOrigin")

Java

String topLevelOrigin = extras.getString("topLevelOrigin");

topLevelCertificateChain

שרשרת האישורים של המוכר (שרשרת האישורים של הקשר העליון של הגלישה). הערך הוא null עבור WebView,‏ localhost או קובץ בדיסק. כל Parcelable הוא חבילה עם מפתח certificate וערך של מערך בייטים.

Kotlin

val topLevelCertificateChain: Array<Parcelable>? =
        extras.getParcelableArray("topLevelCertificateChain")
val list: List<ByteArray>? = topLevelCertificateChain?.mapNotNull { p ->
    (p as Bundle).getByteArray("certificate")
}

Java

Parcelable[] topLevelCertificateChain =
        extras.getParcelableArray("topLevelCertificateChain");
if (topLevelCertificateChain != null) {
    for (Parcelable p : topLevelCertificateChain) {
        if (p != null && p instanceof Bundle) {
            ((Bundle) p).getByteArray("certificate");
        }
    }
}

paymentRequestOrigin

המקור ללא סכימה של הקשר הגלישה ב-iframe שהפעיל את הבונה new PaymentRequest(methodData, details, options) ב-JavaScript. אם הקונסטרוקטור הופעל מההקשר ברמה העליונה, הערך של הפרמטר הזה שווה לערך של הפרמטר topLevelOrigin.

Kotlin

val paymentRequestOrigin: String? = extras.getString("paymentRequestOrigin")

Java

String paymentRequestOrigin = extras.getString("paymentRequestOrigin");

total

מחרוזת ה-JSON שמייצגת את הסכום הכולל של העסקה.

Kotlin

val total: String? = extras.getString("total")

Java

String total = extras.getString("total");

דוגמה לתוכן המחרוזת:

{"currency":"USD","value":"25.00"}

modifiers

הפלט של JSON.stringify(details.modifiers), כאשר details.modifiers מכיל רק את supportedMethods, data ו-total.

paymentRequestId

השדה PaymentRequest.id שאפליקציות של 'תשלום בדחיפה' צריכות לשייך למצב העסקה. אתרי מוכרים ישתמשו בשדה הזה כדי לשלוח שאילתה לאפליקציות של "תשלום בדחיפה" לגבי מצב העסקה מחוץ לפס.

Kotlin

val paymentRequestId: String? = extras.getString("paymentRequestId")

Java

String paymentRequestId = extras.getString("paymentRequestId");

תשובה

הפעילות יכולה לשלוח את התשובה שלה בחזרה דרך setResult עם RESULT_OK.

Kotlin

setResult(Activity.RESULT_OK, Intent().apply {
    putExtra("methodName", "https://bobbucks.dev/pay")
    putExtra("details", "{\"token\": \"put-some-data-here\"}")
})
finish()

Java

Intent result = new Intent();
Bundle extras = new Bundle();
extras.putString("methodName", "https://bobbucks.dev/pay");
extras.putString("details", "{\"token\": \"put-some-data-here\"}");
result.putExtras(extras);
setResult(Activity.RESULT_OK, result);
finish();

צריך לציין שני פרמטרים כתוספים של Intent:

  • methodName: השם של השיטה שבה נעשה שימוש.
  • details: מחרוזת JSON שמכילה את המידע שנדרש למוֹכר כדי להשלים את העסקה. אם הערך של success הוא true, צריך ליצור את details כך ש-JSON.parse(details) יצליח. אם אין נתונים שצריך להחזיר, המחרוזת יכולה להיות "{}", והאתר של המוֹכר יקבל אותה כמילון JavaScript ריק.

אפשר להעביר את הערך RESULT_CANCELED אם המשתמש מבטל את העסקה באפליקציית התשלומים. פעולה כזו תגרום ל-request.show() לדחות את הבקשה עם הערך AbortError באתר של המוכר, כדי לציין שהמשתמש ביטל את העסקה.

Kotlin

setResult(Activity.RESULT_CANCELED)
finish()

Java

setResult(Activity.RESULT_CANCELED);
finish();

החל מ-Chrome 149, יש תמיכה בערכי התוצאות הבאים:

Kotlin

Activity.RESULT_CANCELED // 0 (0x00000000)
Activity.RESULT_OK // -1 (0xffffffff)
const val INTERNAL_PAYMENT_APP_ERROR = Activity.RESULT_FIRST_USER // 1 (0x00000001)

Java

Activity.RESULT_CANCELED // 0 (0x00000000)
Activity.RESULT_OK // -1 (0xffffffff)
static final int INTERNAL_PAYMENT_APP_ERROR = Activity.RESULT_FIRST_USER; // 1 (0x00000001)

אם אפליקציית התשלום נכשלת בגלל שגיאה פנימית, אפשר לציין זאת על ידי העברת Activity.RESULT_FIRST_USER כקוד התוצאה.

אם מוחזרת התוצאה INTERNAL_PAYMENT_APP_ERROR, הפונקציה request.show() תידחה עם השגיאה OperationError באתר של המוכר, מה שמצביע על שגיאה באפליקציית התשלום.

ההבדל בין RESULT_CANCELED (0) לביטול על ידי המשתמש, שגורם ל-AbortError, לבין INTERNAL_PAYMENT_APP_ERROR (1) לשגיאה פנימית באפליקציה, שגורמת ל-OperationError, מאפשר למוֹכרים ליצור תהליכי משתמש טובים יותר.

Kotlin

setResult(Activity.RESULT_FIRST_USER)
finish()

Java

setResult(Activity.RESULT_FIRST_USER);
finish();

אם התוצאה של פעילות בתגובה לתשלום שהתקבלה מאפליקציית התשלום שהופעלה מוגדרת כ-RESULT_OK, ‏ Chrome יבדוק אם יש ערכים לא ריקים ב-methodName וב-details בתוספים שלה. אם האימות נכשל, Chrome יחזיר הבטחה (promise) שנדחתה מ-request.show() עם אחת מהודעות השגיאה הבאות שמוצגות למפתחים:

'Payment app returned invalid response. Missing field "details".'
'Payment app returned invalid response. Missing field "methodName".'

הרשאה

הפעילות יכולה לבדוק את המתקשר באמצעות השיטה getCallingPackage().

Kotlin

val caller: String? = callingPackage

Java

String caller = getCallingPackage();

השלב האחרון הוא אימות אישור החתימה של המתקשר כדי לוודא שלחבילת השיחות יש את החתימה הנכונה.

שלב 4: מאמתים את אישור החתימה של המתקשר

אפשר לבדוק את שם החבילה של המתקשר באמצעות Binder.getCallingUid() ב-IS_READY_TO_PAY, ובאמצעות Activity.getCallingPackage() ב-PAY. כדי לוודא שהגורם שקורא ל-API הוא הדפדפן שאתם רוצים, צריך לבדוק את אישור החתימה שלו ולוודא שהוא תואם לערך הנכון.

אם אתם מטargetים לרמת API‏ 28 ומעלה ומשלבים עם דפדפן שיש לו אישור חתימה יחיד, אתם יכולים להשתמש ב-PackageManager.hasSigningCertificate().

Kotlin

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
)

Java

String packageName =  // The caller's package name
byte[] certificate =  // The correct signing certificate
boolean verified = packageManager.hasSigningCertificate(
        callingPackage,
        certificate,
        PackageManager.CERT_INPUT_SHA256);

השיטה המועדפת לשימוש בדפדפנים עם אישור יחיד היא PackageManager.hasSigningCertificate(), כי היא מטפלת נכון ברוטציה של אישורים. (ל-Chrome יש אישור חתימה יחיד). אי אפשר לבצע רוטציה של אישורי חתימה באפליקציות שיש להן כמה אישורי חתימה.

אם אתם צריכים לתמוך ברמות API 27 ומטה, או אם אתם צריכים לטפל בדפדפנים עם כמה אישורי חתימה, אתם יכולים להשתמש ב-PackageManager.GET_SIGNATURES.

Kotlin

val packageName: String =  // The caller's package name
val expected: Set<String> =  // The correct set of signing certificates

val packageInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES)
val sha256 = MessageDigest.getInstance("SHA-256")
val actual = packageInfo.signatures.map {
    SerializeByteArrayToString(sha256.digest(it.toByteArray()))
}
val verified = actual.equals(expected)

Java

String packageName  =  // The caller's package name
Set<String> expected =  // The correct set of signing certificates

PackageInfo packageInfo =
        packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
Set<String> actual = new HashSet<>();
for (Signature it : packageInfo.signatures) {
    actual.add(SerializeByteArrayToString(sha256.digest(it.toByteArray())));
}
boolean verified = actual.equals(expected);

ניפוי באגים

כדי לראות שגיאות או הודעות מידע, משתמשים בפקודה הבאה:

adb logcat | grep -i pay