איך מתאימים את אפליקציית התשלומים ל-Android לעבודה עם תשלומים באינטרנט ומספקים ללקוחות חוויית משתמש טובה יותר
תאריך פרסום: 5 במאי 2020, תאריך עדכון אחרון: 25 במרץ 2025
Payment Request API מאפשר להציג באתר ממשק מובנה מבוסס-דפדפן שמאפשר למשתמשים להזין את פרטי התשלום הנדרשים בקלות רבה יותר מבעבר. ממשק ה-API יכול גם להפעיל אפליקציות תשלומים ספציפיות לפלטפורמה.
בהשוואה לשימוש רק ב-Intents של Android, תשלומים באינטרנט מאפשרים שילוב טוב יותר עם הדפדפן, האבטחה וחוויית המשתמש:
- אפליקציית התשלום מופעלת כחלון קופץ בהקשר של אתר המוכר.
- ההטמעה היא תוספת לאפליקציית התשלומים הקיימת שלכם, ומאפשרת לכם לנצל את בסיס המשתמשים שלכם.
- המערכת בודקת את החתימה של אפליקציית התשלום כדי למנוע טעינה צדדית.
- אפליקציות תשלומים יכולות לתמוך במספר אמצעי תשלום.
- אפשר לשלב כל אמצעי תשלום, כמו מטבעות וירטואליים, העברות בנקאיות ועוד. אפליקציות תשלומים במכשירי 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 עם התוכן הבא:
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 עם פרטי העסקה בפרמטרים של הכוונה.
אפליקציית התשלומים מגיבה עם 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
שרשרת האישורים של המוכר (שרשרת האישורים של ההקשר ברמה העליונה של הגלישה). Null עבור localhost וקובץ בדיסק, שהם שני הקשרים המאובטחים ללא אישורי SSL. כל Parcelable
הוא חבילה עם מפתח certificate
וערך של מערך בייטים.
val topLevelCertificateChain: Array<Parcelable>? =
extras.getParcelableArray("topLevelCertificateChain")
val list: List<ByteArray>? = topLevelCertificateChain?.mapNotNull { p ->
(p as Bundle).getByteArray("certificate")
}
paymentRequestOrigin
המקור ללא סכימה של הקשר הגלישה ב-iframe שהפעיל את ה-constructor new
PaymentRequest(methodData, details, options)
ב-JavaScript. אם הקריאה ל-constructor בוצעה מההקשר ברמה העליונה, הערך של הפרמטר הזה יהיה שווה לערך של הפרמטר 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 שמכילה את המידע הנדרש כדי שהמוכר יוכל להשלים את העסקה. אם הערך של success הוא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
. כדי לוודא שהמבצע הוא אכן הדפדפן שחשבתם עליו, צריך לבדוק את אישור החתימה שלו ולוודא שהוא תואם לערך הנכון.
אם אתם מטרגטים API ברמה 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 יש אישור חתימה יחיד). לא ניתן לבצע רוטציה של אישורי חתימה באפליקציות שיש להן כמה אישורי חתימה.
אם אתם צריכים לתמוך ברמות API ישנות יותר, ברמה 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) } }