웹 결제와 호환되도록 Android 결제 앱을 조정하고 고객에게 더 나은 사용자 환경을 제공하는 방법을 알아보세요.
Payment Request API는 웹에 내장된 브라우저 기반 인터페이스를 제공하므로 사용자가 필요한 결제 정보를 그 어느 때보다 쉽게 입력할 수 있습니다. 이 API는 플랫폼별 결제 앱을 호출할 수도 있습니다.
Android 인텐트만 사용하는 것과 비교하여 웹 결제는 브라우저, 보안, 사용자 환경과 더 효과적으로 통합할 수 있습니다.
- 결제 앱이 판매자 웹사이트의 컨텍스트에서 모달로 실행됩니다.
- 기존 결제 앱을 보완하여 사용자층을 활용할 수 있도록 구현합니다.
- 사이드로드를 방지하기 위해 결제 앱의 서명이 확인됩니다.
- 결제 앱은 여러 결제 수단을 지원할 수 있습니다.
- 암호화폐, 은행 송금 등 모든 결제 수단을 통합할 수 있습니다. Android 기기의 결제 앱은 기기의 하드웨어 칩에 액세스해야 하는 메서드를 통합할 수도 있습니다.
Android 결제 앱에서 웹 결제를 구현하려면 네 단계를 거쳐야 합니다.
- 판매자가 결제 앱을 찾을 수 있도록 허용합니다.
- 고객에게 결제할 준비가 된 등록된 수단(예: 신용카드)이 있는지 판매자에게 알립니다.
- 고객이 결제하도록 합니다.
- 호출자의 서명 인증서를 확인합니다.
웹 결제의 작동 방식을 확인하려면 android-web-payment 데모를 확인하세요.
1단계: 판매자가 결제 앱을 찾을 수 있도록 허용하기
판매자가 결제 앱을 사용하려면 Payment Request API를 사용하고 결제 수단 식별자를 사용하여 지원하는 결제 수단을 지정해야 합니다.
결제 앱에 고유한 결제 수단 식별자가 있는 경우 브라우저에서 앱을 검색할 수 있도록 자체 결제 수단 매니페스트를 설정할 수 있습니다.
2단계: 고객에게 결제할 준비가 된 등록된 수단이 있는지 판매자에게 알림
판매자는 hasEnrolledInstrument()
를 호출하여 고객이 결제할 수 있는지 쿼리할 수 있습니다. IS_READY_TO_PAY
를 Android 서비스로 구현하여 이 쿼리에 답변할 수 있습니다.
AndroidManifest.xml
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
IS_READY_TO_PAY
서비스의 API는 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()
를 사용하여 호출자가 누구인지 확인할 수 있습니다. 이 작업은 onBind
메서드가 아닌 isReadyToPay
메서드에서 해야 합니다.
override fun isReadyToPay(callback: IsReadyToPayServiceCallback?) {
try {
val callingPackage = packageManager.getNameForUid(Binder.getCallingUid())
// …
호출 패키지에 올바른 서명이 있는지 확인하는 방법은 호출자의 서명 인증서 확인을 참고하세요.
3단계: 고객이 결제하도록 허용
고객은 고객이 결제할 수 있도록 show()
를 호출하여 결제 앱을 실행합니다. 결제 앱은 인텐트 매개변수에 거래 정보가 포함된 Android 인텐트 PAY
를 통해 호출됩니다.
결제 앱은 결제 앱별이며 브라우저에 불투명한 methodName
및 details
로 응답합니다. 브라우저는 JSON 역직렬화를 통해 details
문자열을 판매자의 JavaScript 객체로 변환하지만 그 이상의 유효성은 적용하지 않습니다. 브라우저는 details
를 수정하지 않습니다. 이 매개변수의 값이 판매자에게 직접 전달됩니다.
AndroidManifest.xml
PAY
인텐트 필터가 있는 활동에는 앱의 기본 결제 수단 식별자를 식별하는 <meta-data>
태그가 있어야 합니다.
여러 결제 수단을 지원하려면 <string-array>
리소스가 포함된 <meta-data>
태그를 추가합니다.
<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
는 문자열 목록이어야 하며, 각 문자열은 여기에 표시된 대로 HTTPS 스키마가 있는 유효한 절대 URL이어야 합니다.
<?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>
매개변수
다음 매개변수는 인텐트 추가 항목으로 활동에 전달됩니다.
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
판매자의 결제 페이지(브라우저의 최상위 탐색 컨텍스트)의 <title>
HTML 태그 콘텐츠입니다.
val merchantName: String? = extras.getString("merchantName")
topLevelOrigin
스키마가 없는 판매자의 출처 (최상위 탐색 컨텍스트의 스키마가 없는 출처)입니다. 예를 들어 https://mystore.com/checkout
는 mystore.com
로 전달됩니다.
val topLevelOrigin: String? = extras.getString("topLevelOrigin")
topLevelCertificateChain
판매자의 인증서 체인(최상위 브라우징 컨텍스트의 인증서 체인) localhost 및 디스크의 파일의 경우 Null입니다. 둘 다 SSL 인증서가 없는 안전한 컨텍스트입니다. 각 Parcelable
는 certificate
키와 바이트 배열 값이 있는 Bundle입니다.
val topLevelCertificateChain: Array<Parcelable>? =
extras.getParcelableArray("topLevelCertificateChain")
val list: List<ByteArray>? = topLevelCertificateChain?.mapNotNull { p ->
(p as Bundle).getByteArray("certificate")
}
paymentRequestOrigin
JavaScript에서 new
PaymentRequest(methodData, details, options)
생성자를 호출한 iframe 탐색 컨텍스트의 스키마 없는 출처입니다. 생성자가 최상위 컨텍스트에서 호출된 경우 이 매개변수의 값은 topLevelOrigin
매개변수의 값과 같습니다.
val paymentRequestOrigin: String? = extras.getString("paymentRequestOrigin")
total
총 거래 금액을 나타내는 JSON 문자열입니다.
val total: String? = extras.getString("total")
다음은 문자열의 콘텐츠 예입니다.
{"currency":"USD","value":"25.00"}
modifiers
details.modifiers
에 supportedMethods
와 total
만 포함된 JSON.stringify(details.modifiers)
의 출력입니다.
paymentRequestId
'푸시 결제' 앱이 거래 상태와 연결해야 하는 PaymentRequest.id
필드입니다. 판매자 웹사이트는 이 필드를 사용하여 '푸시 결제' 앱에 대역 외 거래 상태를 쿼리합니다.
val paymentRequestId: String? = extras.getString("paymentRequestId")
응답
활동은 RESULT_OK
를 사용하여 setResult
를 통해 응답을 다시 보낼 수 있습니다.
setResult(Activity.RESULT_OK, Intent().apply {
putExtra("methodName", "https://bobbucks.dev/pay")
putExtra("details", "{\"token\": \"put-some-data-here\"}")
})
finish()
다음 두 가지 매개변수를 인텐트 추가 항목으로 지정해야 합니다.
methodName
: 사용 중인 메서드의 이름입니다.details
: 판매자가 거래를 완료하는 데 필요한 정보가 포함된 JSON 문자열입니다. 성공이true
인 경우JSON.parse(details)
가 성공하도록details
를 구성해야 합니다.
결제 앱에서 거래가 완료되지 않은 경우(예: 사용자가 결제 앱에서 계정의 올바른 PIN 코드를 입력하지 않은 경우) 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단계: 호출자의 서명 인증서 확인
IS_READY_TO_PAY
에서는 Binder.getCallingUid()
로, PAY
에서는 Activity.getCallingPackage()
를 사용하여 호출자의 패키지 이름을 확인할 수 있습니다. 호출자가 의도한 브라우저인지 실제로 확인하려면 서명 인증서를 확인하고 올바른 값과 일치하는지 확인해야 합니다.
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) } }