Android 결제 앱 개발자 가이드

Android 결제 앱이 웹 결제와 호환되도록 조정하고 고객에게 더 나은 사용자 환경을 제공하는 방법을 알아보세요.

Payment Request API는 웹 기반 기본 제공 브라우저 기반 인터페이스를 통해 사용자가 결제 금액을 입력할 수 있습니다. 훨씬 더 쉽게 정보를 얻을 수 있습니다. API는 플랫폼별 결제를 호출할 수도 있습니다. 있습니다.

브라우저 지원

  • Chrome: 60. <ph type="x-smartling-placeholder">
  • Edge: 15. <ph type="x-smartling-placeholder">
  • Firefox: 깃발 뒤쪽에 있습니다.
  • Safari: 11.1. <ph type="x-smartling-placeholder">

소스

<ph type="x-smartling-placeholder">
</ph>
웹 결제를 사용하는 플랫폼별 Google Pay 앱의 결제 흐름

Android 인텐트만 사용하는 것에 비해 웹 결제는 더 나은 통합 가능 사용자 환경을 개선할 수 있습니다.

  • 결제 앱이 판매자 웹사이트의 컨텍스트에서 모달로 실행됩니다.
  • 구현은 기존 결제 앱을 보완하는 것으로, 다음 작업을 할 수 있습니다. 활용할 수 있습니다
  • 결제 앱의 서명이 사이드로드입니다.
  • 결제 앱은 여러 결제 수단을 지원할 수 있습니다.
  • 암호화폐, 은행 송금 등 모든 결제 수단을 통합되었습니다. Android 기기의 결제 앱은 기기의 하드웨어 칩에 액세스해야 합니다.
를 통해 개인정보처리방침을 정의할 수 있습니다.

Android 결제 앱에서 웹 결제를 구현하려면 네 단계를 거쳐야 합니다.

  1. 판매자가 내 결제 앱을 찾을 수 있도록 허용합니다.
  2. 고객에게 등록된 결제 수단 (예: 크레딧)이 있는지 판매자에게 알려주세요. 카드)가 표시됩니다.
  3. 고객이 결제할 수 있도록 합니다.
  4. 호출자의 서명 인증서를 확인합니다.

웹 결제가 어떻게 작동하는지 확인하려면 android-web-payment 데모를 들을 수 있습니다.

1단계: 판매자가 결제 앱을 찾을 수 있도록 허용하기

판매자가 결제 앱을 사용하려면 Payment(결제) API 요청 및 결제 수단을 사용하여 지원하는 결제 수단 지정 사용됩니다.

결제 앱 고유의 결제 수단 식별자가 있는 경우 직접 결제 수단을 설정할 수 있음 매니페스트를 사용하여 브라우저가 찾을 수 있습니다.

2단계: 고객에게 결제 준비가 된 결제 수단이 등록되어 있는지 판매자에게 알림

판매자는 hasEnrolledInstrument()를 호출하여 고객이 결제할 수 있습니다. 다음과 같은 작업을 할 수 있습니다. 이 쿼리에 답하기 위한 Android 서비스로 IS_READY_TO_PAY를 구현합니다.

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

결제 앱이 결제 앱인 methodNamedetails로 응답합니다. 브라우저에 대해 불투명합니다. 브라우저가 details 문자열을 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을 입력합니다.

<?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 컨텍스트와 연동됩니다 각 Parcelablecertificate 키 및 바이트 배열 값.

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

JSON.stringify(details.modifiers)의 출력(여기서 details.modifiers) supportedMethodstotal만 포함합니다.

paymentRequestId

'푸시-결제'를 수행하는 PaymentRequest.id 필드 앱과 연결해야 하는 경우 트랜잭션 상태입니다. 판매자 웹사이트는 이 필드를 사용하여 &quot;push-payment&quot; 앱의 대역 외 거래 상태를 확인할 수 있습니다.

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: 판매자가 거래를 완료할 수 있습니다 성공이 true인 경우 details는 구성되어야 합니다.JSON.parse(details)

다음에서 거래가 완료되지 않은 경우 RESULT_CANCELED를 전달할 수 있습니다. 예를 들어 사용자가 결제 앱에서 사용할 수 있는 올바른 PIN 코드를 결제할 수 있습니다 브라우저에서 사용자가 확인할 수 있습니다.

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_PAYPAY에서 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) } }