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

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

יואיצ'י ארקי
יואיצ'י ארקי

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

תמיכה בדפדפן

  • 60
  • 15
  • 11.1

מקור

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

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

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

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

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

כדי לראות את התהליך של Web Payments, ראו android-web-payment.

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

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

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

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

paymentRequestOrigin

המקור ללא סכימה של הקשר הגלישה ב-iframe שהפעיל את הבנאי 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) } }