במאמר הזה מוסבר איך להתאים את אפליקציית התשלומים ל-Android כך שתפעל עם תשלומים באינטרנט ותספק ללקוחות חוויית משתמש טובה יותר.
פורסם: 5 במאי 2020, עודכן לאחרונה: 27 במאי 2025
Payment Request API (ממשק API לבקשות תשלום) מספק ממשק מובנה מבוסס-דפדפן באינטרנט, שמאפשר למשתמשים להזין את פרטי התשלום הנדרשים בקלות יותר מאי פעם. ממשק ה-API יכול גם להפעיל אפליקציות תשלום ספציפיות לפלטפורמה.
בהשוואה לשימוש רק ב-Android Intents, תשלומים באינטרנט מאפשרים שילוב טוב יותר עם הדפדפן, האבטחה וחוויית המשתמש:
- אפליקציית התשלום נפתחת כחלון מודאלי בהקשר של אתר המוכר.
- ההטמעה היא בנוסף לאפליקציית התשלומים הקיימת, ומאפשרת לכם לנצל את בסיס המשתמשים שלכם.
- החתימה של אפליקציית התשלום נבדקת כדי למנוע התקנה ממקור לא ידוע.
- אפליקציות תשלום יכולות לתמוך בכמה אמצעי תשלום.
- אפשר לשלב כל אמצעי תשלום, כמו מטבעות קריפטוגרפיים, העברות בנקאיות ועוד. אפליקציות תשלום במכשירי Android יכולות אפילו לשלב שיטות שדורשות גישה לשבב החומרה במכשיר.
כדי להטמיע תשלומים באתר באפליקציית תשלומים ל-Android, צריך לבצע ארבעה שלבים:
- כך מוכרים יכולים לגלות את אפליקציית התשלום שלכם.
- העברת מידע למוכר אם ללקוח יש אמצעי תשלום רשום (כמו כרטיס אשראי) שאפשר לשלם איתו.
- אפשרות ללקוח לבצע תשלום.
- מאמתים את אישור החתימה של המתקשר.
כדי לראות את התכונה 'תשלומים באינטרנט' בפעולה, אפשר לעיין בהדגמה של 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:
packageNamemethodNamesmethodDatatopLevelOriginpaymentRequestOrigintopLevelCertificateChain
הסמל 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:
methodNamesmethodDatamerchantNametopLevelOrigintopLevelCertificateChainpaymentRequestOrigintotalmodifierspaymentRequestIdpaymentOptionsshippingOptions
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