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

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

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

תמיכה בדפדפן

  • Chrome: 60.
  • קצה: 15.
  • Firefox: מאחורי דגל.
  • Safari: 11.1.

מקור

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

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

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

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

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

כדי לראות את התכונה 'תשלומים באינטרנט', אפשר לעבור אל android-web-payment .

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

כדי שמוֹכר יוכל להשתמש באפליקציית התשלומים שלכם, הוא צריך להשתמש בכלי Payment בקשת 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() כדי להפעיל את התשלום אפליקציה כדי שהלקוח יוכל לבצע תשלום. אפליקציית התשלומים מופעלת דרך Android Intent PAY עם פרטי עסקאות בפרמטרים של 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. אם הופעל מההקשר ברמה העליונה, אז הערך של שווה לערך של הפרמטר 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" צריכות להיות משויכות אל על מצב העסקה. אתרים של מוכרים ישתמשו בשדה הזה כדי לשלוח שאילתה על &quot;push-payment&quot; אפליקציות למצב של עסקאות מחוץ למסגרת.

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) } }