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

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

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

תמיכה בדפדפנים

  • Chrome:‏ 60.
  • Edge:‏ 15.
  • Firefox: מאחורי דגל.
  • Safari: 11.1.

מקור

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

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

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

יש ארבעה שלבים להטמעת תשלומים באינטרנט באפליקציית תשלומים ל-Android:

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

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

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 שהפעיל את המבנה 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) } }