Integrating Google Pay to Android App

Photo by Jamie Davies on Unsplash
Photo by Jamie Davies on Unsplash
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.

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 .

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.

Google Pay API Test Cards Allowlist
Google Pay API Test Cards Allowlist

Click Join group. Then, your Google Account will have test cards.

Join Google Pay API Test Cards Allowlist
Join Google Pay API Test Cards Allowlist

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.

Google Pay UI.
Google Pay UI.

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

Leave a Reply

Your email address will not be published. Required fields are marked *

You May Also Like