במאמר הזה מוסבר איך להתאים את אפליקציית התשלומים ב-Android כך שיפעל עם תשלומים באינטרנט, ולספק חוויית משתמש טובה יותר ללקוחות.
Payment Request API מספק לאינטרנט ממשק מובנה מבוסס דפדפן, שמאפשר למשתמשים להזין את פרטי התשלום הנדרשים בקלות רבה יותר מאי פעם. ה-API יכול גם להפעיל אפליקציות תשלום ספציפיות לפלטפורמה.
בהשוואה לשימוש באובייקטים של Intent של Android בלבד, תשלומים באינטרנט מאפשרים שילוב טוב יותר עם הדפדפן, האבטחה וחוויית המשתמש:
- אפליקציית התשלומים מופעלת כמודל בהקשר של אתר המוכר.
- ההטמעה היא משלימה לאפליקציית התשלומים הקיימת, וכך אתם יכולים לנצל את בסיס המשתמשים שלכם.
- החתימה של אפליקציית התשלומים מסומנת כדי למנוע התקנה ממקור לא ידוע.
- אפליקציות תשלום יכולות לתמוך בכמה אמצעי תשלום.
- אפשר לשלב כל אמצעי תשלום, כמו מטבע וירטואלי, העברות בנקאיות ועוד. אפליקציות תשלום במכשירי Android יכולות אפילו לשלב שיטות שמצריכות גישה לצ'יפ החומרה במכשיר.
יש ארבעה שלבים להטמעת תשלומים באינטרנט באפליקציית תשלומים ל-Android:
- רוצה לאפשר למוכרים לגלות את אפליקציית התשלומים שלך?
- מיידעים את המוכר אם הלקוח מחזיק באמצעי תשלום רשום (כמו כרטיס אשראי) שמוכן לתשלום.
- מאפשרים ללקוח לבצע תשלום.
- מאמתים את אישור החתימה של המתקשר.
כדי לראות את התכונה 'תשלומים באינטרנט', תוכלו לצפות בהדגמה של android-web-payment.
שלב 1: מאפשרים למוכרים לגלות את אפליקציית התשלומים שלכם
כדי שמוכרים יוכלו להשתמש באפליקציית התשלומים שלכם, הם צריכים להשתמש ב-PaymentRequest 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
הוא אופציונלי. אם אין handler כזה באפליקציית התשלומים, דפדפן האינטרנט יוצא מנקודת הנחה שהאפליקציה תמיד יכולה לבצע תשלומים.
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, עם פרטי עסקאות בפרמטרים של Intent.
אפליקציית התשלומים מגיבה באמצעות methodName
ו-details
, שהם אפליקציות ספציפיות
לאפליקציית תשלומים ואטומים לדפדפן. הדפדפן ממיר את המחרוזת details
לאובייקט JavaScript עבור המוכר באמצעות פעולת deserialization של 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/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
שאפליקציות 'push-payment' צריכות לשייך למצב העסקה. אתרים של מוכרים ישתמשו בשדה הזה כדי לשלוח שאילתה על אפליקציות ה'דחיפת תשלומים' לגבי מצב העסקה מחוץ למסגרת.
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()
צריך לציין שני פרמטרים כתוספות Intent:
methodName
: שם השיטה שבה אתם משתמשים.details
: מחרוזת JSON שמכילה את המידע שדרוש למוכר כדי להשלים את העסקה. אם ההצלחה היא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) } }