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

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

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

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

Browser Support

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

Source

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

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

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

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

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

פרמטרים

החבילה parameters נוספה ב-Chrome 139. תמיד צריך לבדוק את הערך שלו מול הערך של null.

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

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

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

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

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

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

אפליקציית התשלומים מגיבה עם 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/chromium_payment_method_names" />
</activity>

השדה android:resource חייב להיות רשימה של מחרוזות, שכל אחת מהן חייבת להיות כתובת 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:

  • 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 שהפעיל את ה-constructor‏ new PaymentRequest(methodData, details, options) ב-JavaScript. אם הקריאה ל-constructor בוצעה מההקשר ברמה העליונה, הערך של הפרמטר הזה יהיה שווה לערך של הפרמטר 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("instrumentDetails", "{\"token\": \"put-some-data-here\"}");
result.putExtras(extras);
setResult(Activity.RESULT_OK, result);
finish();

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

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

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

Kotlin

setResult(Activity.RESULT_CANCELED)
finish()

Java

setResult(Activity.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() שלה.

Kotlin

val caller: String? = callingPackage

Java

String caller = getCallingPackage();

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

שלב 4: אימות אישור החתימה של מבצע הקריאה החוזרת

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

אם אתם מטרגטים רמת 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