Google Pay 是 Google 的行動支付服務。我們可以用 Google Pay 在實體店家的 NFC 刷卡,也可以在 apps 中購買商品。另外,Google Wallet 可以管理信用卡,所以我們不需要隨身攜帶數張信用卡。本文章將介紹,如何將 Google Pay 整合至 Android app。
Table of Contents
準備測試環境
在開始寫程式之前,讓我們先設定好測試環境。在本文章中,我們將在 Android emulaltor 中,使用 Google Pay 付款。
首先,這個 emulator 必須要支援 Google Play。在 emulator 的 Settings 中,登入你的 Google Account。然後,打開 test card suite group,如下圖。
點擊 Join group
。這樣你的 Google Account 就會有測試的 cards。
引入依賴
在 app level 的 build.gradle.kts 中,加入以下依賴。
dependencies { implementation("com.google.android.gms:play-services-wallet:19.3.0-beta01") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.2") }
初始化 PayButton
在 main_activity.xml 中,宣告 PayButton。
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <com.google.android.gms.wallet.button.PayButton android:id="@+id/pay_button" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
在 MainActivity 中,初始化 PayButton。我們可以藉由 setButtonTheme() 和 setButtonType() 來設定 PayButton 的樣式與標題。更多的詳情,請參考 Brand guidelines。
class MainActivity : AppCompatActivity() { companion object { private const val TAG = "MainActivity" } private lateinit var binding: MainActivityBinding private val viewModel: MainViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = MainActivityBinding.inflate(layoutInflater) setContentView(binding.root) initPayButton() } private fun initPayButton() { try { val buttonOptions = ButtonOptions.newBuilder() .setButtonTheme(ButtonTheme.LIGHT) .setButtonType(ButtonType.PAY) .setAllowedPaymentMethods(viewModel.getAllowedPaymentMethods().toString()) .build() binding.payButton.initialize(buttonOptions) binding.payButton.setOnClickListener { // TODO: pay } } catch (e: JSONException) { Log.e(TAG, "Error on getting payment methods", e) } } }
除了 PayButton 的樣式之外,我們還要設定 allowed payment methods。Google Pay API 並沒有提供相關的資料結構,而是使用 JSON。其 JSON 的內容定義在 PaymentMethod。
class MainViewModel(application: Application) : AndroidViewModel(application) { fun getAllowedPaymentMethods(withToken: Boolean): JSONArray { val jsonObject = getCardPaymentMethod(withToken) return JSONArray().put(jsonObject) } private fun getCardPaymentMethod(): JSONObject { return JSONObject().apply { put("type", "CARD") put("parameters", JSONObject().apply { put("allowedAuthMethods", JSONArray().apply { put("PAN_ONLY") put("CRYPTOGRAM_3DS") }) put("allowedCardNetworks", JSONArray().apply { put("AMEX") put("DISCOVER") put("JCB") put("MASTERCARD") put("VISA") }) put("billingAddressRequired", true) put("billingAddressParameters", JSONObject().apply { put("format", "FULL") }) }) } } }
檢查 Google Pay 是否可用
設定好 PayButton 之後,我們要檢查 Google Pay 是否 ready。如果 Google Pay 是 ready 的話,我們才顯示 PayButton。
class MainActivity : AppCompatActivity() { ... private fun initPayButton() { ... binding.payButton.visibility = View.INVISIBLE viewModel.isGooglePayReady.observe(this) { if (it) { binding.payButton.visibility = View.VISIBLE } else { binding.payButton.visibility = View.INVISIBLE Toast.makeText( this, "Google Pay is not available on this device", Toast.LENGTH_LONG ).show() } } } }
在 MainViewModel 中,我們宣告 PaymentsClient,並且設定它使用測試環境。利用 PaymentsClient.isReadyToPay()
來檢查是否可以在此裝置上使用 Google Pay。它需要一個 IsReadyToPayRequest。與在初始化 PayButton 時一樣,我們必須要設定 allowed payment methods。
class MainViewModel(application: Application) : AndroidViewModel(application) { companion object { private const val TAG = "MainViewModel" } val isGooglePayReady: MutableLiveData<Boolean> = MutableLiveData() private val paymentsClient: PaymentsClient init { val walletOptions = Wallet.WalletOptions.Builder() .setEnvironment(WalletConstants.ENVIRONMENT_TEST) .build() paymentsClient = Wallet.getPaymentsClient(application, walletOptions) viewModelScope.launch { checkIfGooglePayIsReady() } } private suspend fun checkIfGooglePayIsReady() { try { val jsonRequest = JSONObject().apply { put("apiVersion", 2) put("apiVersionMinor", 0) put("allowedPaymentMethods", getAllowedPaymentMethods()) } val request = IsReadyToPayRequest.fromJson(jsonRequest.toString()) Log.d(TAG, jsonRequest.toString()) val task = paymentsClient.isReadyToPay(request) if (task.await()) { isGooglePayReady.postValue(true) } else { Log.e(TAG, "Error on requesting if Google Pay is ready") isGooglePayReady.postValue(false) } } catch (e: Exception) { Log.e(TAG, "Error on checking if Google Pay is ready", e) isGooglePayReady.postValue(false) } } }
執行 Payment
最後一步就是付款了。我們要呼叫 PaymentsClient.loadPaymentData()
來執行付款。它要求一個 PaymentDataRequest。在 PaymentDataRequest 中,我們需要填入付款的金額、貨幣和 PaymentMethod。與設定 PayButton 和呼叫 PaymentsClient.isReadyToPay()
時不一樣的是,我們還要在 PaymentMethods 中,設定 TokenizationSpecification。我們需要在 TokenizationSpecification 中填入 gateway 和 merchant ID。
當 PaymentsClient.loadPaymentData()
回傳 true 時,表示付款完成。如果它回傳 false 時,且其 exception 是 ResolvableApiException
的話,表示還沒有進行付款,因為它要求我們要顯示付款的畫面。我們可以藉由 task.resolution
取得一個 PendingIntent
。
class MainViewModel(application: Application) : AndroidViewModel(application) { private fun getCardPaymentMethod(withToken: Boolean): JSONObject { return JSONObject().apply { put("type", "CARD") put("parameters", JSONObject().apply { put("allowedAuthMethods", JSONArray().apply { put("PAN_ONLY") put("CRYPTOGRAM_3DS") }) put("allowedCardNetworks", JSONArray().apply { put("AMEX") put("DISCOVER") put("JCB") put("MASTERCARD") put("VISA") }) put("billingAddressRequired", true) put("billingAddressParameters", JSONObject().apply { put("format", "FULL") }) }) if (withToken) { put("tokenizationSpecification", JSONObject().apply { put("type", "PAYMENT_GATEWAY") put("parameters", JSONObject().apply { put("gateway", "example") put("gatewayMerchantId", "exampleGatewayMerchantId") }) }) } } } fun pay(priceCents: Long) { getPaymentDataRequest(priceCents).addOnCompleteListener { task -> if (task.isSuccessful) { paymentSuccess.postValue(task.result) } else { when (val e = task.exception) { is ResolvableApiException -> paymentUi.postValue(e.resolution) is ApiException -> paymentError.postValue(e) else -> paymentError.postValue(e) } } } } private fun getPaymentDataRequest(priceCents: Long): Task<PaymentData> { // ex: If priceCents is 950, price will be 9.50 val price = BigDecimal(priceCents) .divide(BigDecimal(100)) .setScale(2, RoundingMode.HALF_EVEN) .toString() val jsonRequest = JSONObject().apply { put("apiVersion", 2) put("apiVersionMinor", 0) put("allowedPaymentMethods", getAllowedPaymentMethods(true)) put("transactionInfo", JSONObject().apply { put("totalPrice", price) put("totalPriceStatus", "FINAL") put("countryCode", "US") put("currencyCode", "USD") }) put("merchantInfo", JSONObject().put("merchantName", "Example Merchant")) put("shippingAddressParameters", JSONObject().apply { put("phoneNumberRequired", false) put("allowedCountryCodes", JSONArray(listOf("US", "GB"))) }) put("shippingAddressRequired", true) } val request = PaymentDataRequest.fromJson(jsonRequest.toString()) Log.d(TAG, jsonRequest.toString()) return paymentsClient.loadPaymentData(request) } }
class MainActivity : AppCompatActivity() { companion object { private const val TAG = "MainActivity" } private lateinit var binding: MainActivityBinding private val viewModel: MainViewModel by viewModels() private val resolvePaymentForResult = registerForActivityResult( ActivityResultContracts.StartIntentSenderForResult() ) { result: ActivityResult -> when (result.resultCode) { RESULT_OK -> { val resultData = result.data if (resultData != null) { val paymentData = PaymentData.getFromIntent(resultData) paymentData?.let { handlePaymentSuccess(it) } } } RESULT_CANCELED -> { // The user cancelled the payment } } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = MainActivityBinding.inflate(layoutInflater) setContentView(binding.root) initPayButton() initViewModel() } ... private fun initViewModel() { viewModel.paymentUi.observe(this) { resolvePaymentForResult.launch(IntentSenderRequest.Builder(it).build()) } viewModel.paymentSuccess.observe(this) { handlePaymentSuccess(it) } viewModel.paymentError.observe(this) { Log.e(TAG, "Payment failed", it) } } private fun handlePaymentSuccess(paymentData: PaymentData) { val json = JSONObject(paymentData.toJson()) val token = json.getJSONObject("paymentMethodData") .getJSONObject("tokenizationData") .optString("token") Log.d(TAG, "token = $token") // Send the token to the server startActivity(Intent(this, PaymentSuccessActivity::class.java)) } }
我們 launch 這個 PendingIntent
後,就會出現以下的付款畫面。我們可以看到帳號中已經有一些測試的 cards。
付款成功後,我們會收到一個 PaymentData response。我們可以從這個 response 中取得一個 token,最後將這個 token 送到 server。
結語
Google Pay 的整合並不難,而且官方還提供了範例程式碼,其中有 Java + XML 和 Kotlin + Compose。另外,它的 requests 和 responses 都是 JSON,使得使用起來不是那麼地方便。
參考
- Guides, Google Pay for Payments.
- Client reference, Google Pay for Payments.
- JSON request objects, Google Pay for Payments.
- JSON response objects, Google Pay for Payments.
- android-quickstart.