Google Pay is Google’s mobile payment service. We can use Google Pay make a tap-to-pay in physical stores, and we can also purchase goods in apps. In addition, Google Wallet can manage credit cards, so we don’t need to carry several credit cards. This article will introduce how to integrate Google Pay into Android app.
The complete code for this chapter can be found in .
Table of Contents
Preparing Test Environment
Before we start writing the program, let’s set up the testing environment. In this article, we will use Google Pay for payment in Android emulaltor.
First of all, this emulator must support Google Play. In the Settings app of the emulator, login with your Google Account. Then, open test card suite group, as shown below.
Click Join group
. Then, your Google Account will have test cards.
Dependencies
In the build.gradle.kts at app level, add the following dependencies.
dependencies { implementation("com.google.android.gms:play-services-wallet:19.3.0-beta01") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.2") }
Initializing PayButton
In main_activity.xml, declare 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>
In MainActivity, initialize PayButton. We can set the style and title of PayButton through setButtonTheme() and setButtonType(). For more details, please refer to 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) } } }
In addition to the PayButton style, we also need to set the allowed payment methods. Google Pay API does not provide relevant data structure, but uses JSON. Its JSON content is defined in 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") }) }) } } }
Checking If Google Pay is Ready
After setting up the PayButton, we need to check whether Google Pay is ready. We only display the PayButton if Google Pay is ready.
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() } } } }
In MainViewModel, we declare PaymentClient and configure it to use the test environment. Use PaymentsClient.isReadyToPay()
to check if Google Pay can be used on this device. It requires an IsReadyToPayRequest. As when initializing the PayButton, we must set the 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) } } }
Executing Payment
The last step is payment. We need to call PaymentClient.loadPaymentData()
to execute a payment. It requires a PaymentDataRequest. In PaymentDataRequest, we need to fill in the payment amount, currency and PaymentMethod. Different from setting PayButton and calling PaymentsClient.isReadyToPay()
, we also need to set TokenizationSpecification in PaymentMethods. We need to fill in the gateway and merchant ID in the TokenizationSpecification.
When PaymentsClient.loadPaymentData()
returns true, it means the payment is succeeded. If it returns false and its exception is ResolvableApiException
, it means that the payment has not been made because it requires us to display the payment screen. We can get a PendingIntent
through task.resolution
.
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)) } }
After we launch this PendingIntent
, the following payment screen will appear. We can see that there are already some test cards in the account.
After the payment is successful, we will receive a PaymentData response. We can get a token from this response and finally send the token to the server.
Conclusion
The integration of Google Pay is not difficult, and the official also provides sample code, including Java + XML and Kotlin + Compose. In addition, its requests and responses are both JSON, which makes it not so convenient to use.
Reference
- 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.