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

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

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

תמיכה בדפדפן

  • 60
  • 15
  • 11.1

מקור

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

בהשוואה לשימוש באובייקטים של Intent של 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 הוא אופציונלי. אם אין 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) } }