Recently, after developing a new product called Yip, which was put on The Google Store, I received feedback from foreign users that they wanted to use Google Pay, so I planned to integrate Google Pay into my app to allow users to make in-app purchases.

I haven’t integrated Google Pay at all before, and from what I’ve found, the way Google Pay has been developed has been changed and is much simpler than the way it was developed using AIDL.

1. Client integration

1.1 Documents

For client integration, the official documentation has given a detailed description. The corresponding documentation is as follows:

  • Integration of library document “to Google Play library integrated into your application on the settlement, link: developer. The android, Google. Cn/Google/Play…

Although the documentation integration instructions are very detailed, sometimes we need to know the meaning of specific classes and their fields. Class documentation can refer to:

  • Class documentation: developer. The android. Google. Cn/reference/c…

1.2 Some details of integration

Since Google’s official documentation has been more detailed, here I will only introduce some details of the design of their integration.

1. Dependency isolation

Remember that when you installed APK with the purchase feature, you were prompted to “need Google service to use”, so I prepared in advance during development. What I would like is for Gooogle Billing to be included only in the foreign version, which can be configured at packaging time, and not in the domestic version. So, in addition to the app Module, we need to add a module for Google Pay. This module is only used to handle Google settlement and has the following dependencies:

dependencies {
    // billing
    compileOnly "Com. Android. Billingclient: billing: 3.0.0"
    compileOnly "Com. Android. Billingclient: billing - KTX: 3.0.0"
    // ...
}
Copy the code

I’m using compileOnly so that access to Google Clearing can be configured in the main project module. In addition, the code uses reflection to get the class to determine whether the Google settlement dependency has been added:

private fun isGoogleDependencyAdded(a): Boolean = try {
    Class.forName("com.android.billingclient.api.BillingClient")
    true
} catch (e: ClassNotFoundException) {
    false
}
Copy the code

In addition, we need to make sure that we don’t directly reference the Google Clearing library classes in our main project. So, we need to wrap the classes of the Google Clearing library,

data class SkuDetailsWrapper(val skuDetails: SkuDetails) {

    /** Get the product id */
    fun getProductId(a): String = skuDetails.sku

    /** Get sku price */
    fun getPrice(a): String = skuDetails.price

    /** Launch billing flow */
    fun launch(activity: Activity, onSuccess: () -> Unit, onFailed: (msg: String) - >Unit) {
        BillingManager.instance.launchBillingFlow(activity, skuDetails, onSuccess, onFailed)
    }
}

/** Purchase wrapper */
data class PurchaseWrapper(val purchase: Purchase) {

    fun getPackageName(a): String = purchase.packageName

    fun getPurchaseToken(a): String = purchase.purchaseToken

    fun isAcknowledged(a): Boolean = purchase.isAcknowledged

    fun getOrderId(a): String = purchase.orderId

    /** Get google goods id */
    fun getGoogleGoodsId(a): String = purchase.sku

    /** Is given product was purchased */
    fun isPurchased(a): Boolean = purchase.purchaseState == Purchase.PurchaseState.PURCHASED
}

/** Billing result wrapper */
data class BillingResultWrapper(val billingResult: BillingResult)
Copy the code

This way we only need to expose our own packaging class. Free to decide whether or not to rely on Google billing when packaging.

2. Reconnect logic

Google Settlement service needs to be reconnected when it is used again, and there is no guarantee that each connection will be successful, so we need to try again, and we can connect first every time we request Google service. In addition, The Google Clearing library does not allow multiple connections, so some controls are needed to prevent too many requests.

/** Play service connection callback */
interface OnConnectedListener {
    fun onConnected(a)
}

class BillingManager private constructor() {
    private val connectListeners = mutableListOf<OnConnectedListener>()

    private var connect = AtomicInteger(STATE_DISCONNECTED)

    /** Connect Google billing with retry logic. */
    private fun connect(onConnected: () -> Unit, onFailed: (msg: String) - >Unit) {
        // batch the callbacks to avoid too much connections
        connectListeners.add(object : OnConnectedListener {
            override fun onConnected(a) {
                onConnected()
            }
        })
        if (connect.get() == STATE_CONNECTED) {
            connectListeners.forEach { it.onConnected() }
            connectListeners.clear()
        } else {
            if (connect.get() == STATE_DISCONNECTED) {
                connect.set(STATE_CONNECTING)
                doConnect({
                    connectListeners.forEach { it.onConnected() }
                    connectListeners.clear()
                    // force to reconnect service next time
                    connect.set(STATE_DISCONNECTED)
                }, onFailed)
            }
        }
    }

    /** Do real connection. */
    private fun doConnect(onConnected: () -> Unit, onFailed: (msg: String) - >Unit) {
        billingClient.startConnection(object : BillingClientStateListener {
            override fun onBillingServiceDisconnected(a) {
                L.d("onBillingServiceDisconnected")
                connect.set(STATE_DISCONNECTED)
            }

            override fun onBillingSetupFinished(p0: BillingResult) {
                L.d("Setup finished: ${p0.responseCode} ${p0.debugMessage}")
                if (p0.responseCode == BillingClient.BillingResponseCode.OK) {
                    onConnected()
                } else {
                    L.d("Setup finished with failure: ${p0.responseCode} ${p0.debugMessage}.")
                    onFailed("Failed: ${p0.responseCode} ${p0.debugMessage}"}}})}}Copy the code

AtomInteger is used to mark the current state, and if the connection is currently under way, connectListeners will collect the requests and trigger all of them after successful connections. Thus, when we need to make a request, all we need to do is call:

    fun queryInAppPurchase(onSuccess: () -> Unit = {}, onFailed: (msg: String) - >Unit = {}) {
        connect({
            GlobalScope.launch(Dispatchers.Main) {
                val purchaseResult = withContext(Dispatchers.IO) {
                    billingClient.queryPurchases(BillingClient.SkuType.INAPP)
                }
                // ... do one get result
            }
        }, onFailed)
    }
Copy the code

Following the above configuration, calling launchBillingFlow after querying the product triggers the payment dialog. The app is then uploaded to the Google Play Console and tested for purchase after a tester is configured. There are a few points that need to be paid attention to, otherwise it may lead to some lost orders and security problems, for the overall purchase process design in the server side verification part together.

1.3 Some problems encountered

1. Google Play in-app Billing API version is less than 3

Google Play In-app Billing API Version is Less than 3. I ran into this problem when I was testing. To sum up: this error occurs when the area of your account is identified as domestic. So I had no choice but to register several new Google accounts, and then set the location abroad.

2. Verify the server

It should be noted that after the user completes the payment, you need to confirm the purchase in the code before the payment is completed. Otherwise, the refund process will be carried out. There is some design logic to be aware of here, because on the one hand you want to prevent hackers from masquerading Google’s response; On the other hand, you should avoid losing orders. It’s safe to think of client-side logic as unreliable, because you can’t guarantee that the user will follow through on the flow you expect, and the flow can fail as a result of an unexpected node exit, so the design logic here is worth spending some time thinking about.

For server-side validation, Google has tripartite libraries that offer Java and Python versions. But, uh, I tried it, and it didn’t work very well. Some of the validation logic here can be implemented using pure HTTP requests without the need for a tripartite library. Documentation and articles on server-side validation are really sparse, and background configuration can be tricky, especially when you’re doing it for the first time.

2.1 Use of product Verification interfaces

1. Platform configuration

For payment verification, Google provides an interface to query product information. When the client completes the purchase, it gets a purchaseToken, which we can use to request Google’s interface to determine if the order is valid (hackers can fake Google’s return).

This interface and configuration is cumbersome, and documentation is minimal.

For configuration, refer to the Google Play Developer apis use primer, link: developers. Google. Cn/android – pub…

In short, you do

  • Google Cloud projects are now associated with API access on the Google Play Console
  • Then go to the Google APIs to create the OAuth 2.0 client ID credentials
  • There are several types of clients. In this example, Web application is selected
  • After creating the Web application, you will get client_id and redirect_URI. The redirect_URI is a link you specify, any address will do, and it is used to make a jump. The jump address will have a code parameter attached to it, and the purpose here is to get this parameter.

2. Application authorization: Obtain the code parameter

The application part authorization request reference documentation: developers.google.com/android-pub…

Splicing the above OAuth client creation information into a URL, using the following template

Accounts.google.com/o/oauth2/au… .

Then visit this link. You will jump to your specified redirect_URI as described above, and get the code parameter from the address after the jump. The code here is like a key that we need to use to get refresh_token and access_token. There are some potholes, which we will explain later.

3. Obtain refresh_token and access_token

When querying product information, we need to use the access_token, but the access_token will expire in about one hour. After expiration we need to use refresh token to access Google link to get new access token. I still use OkHTtp+Retrofit for server side HTTP requests. The Retorfit interfaces are as follows:

// base url: https://accounts.google.com/o/oauth2/
public interface GooglePlayOAuthApi {

    @FormUrlEncoded
    @POST("token")
    Call<GoogleOAuthResponse> oauth(@Field("grant_type") String grantType,
                                    @Field("code") String code,
                                    @Field("client_id") String clientId,
                                    @Field("client_secret") String clientSecret,
                                    @Field("redirect_uri") String redirectUri);

    @FormUrlEncoded
    @POST("token")
    Call<GoogleOAuthResponse> refreshToken(@Field("grant_type") String grantType,
                                           @Field("refresh_token") String refreshToken,
                                           @Field("client_id") String clientId,
                                           @Field("client_secret") String clientSecret);

}
Copy the code

The response data structure is as follows:

@Data
public class GoogleOAuthResponse {
    private String access_token;
    private String token_type;
    private int expires_in;
    private String refresh_token;
}
Copy the code

These are two form requests, from top to bottom, to obtain the first access token and the refresh token for subsequent updates.

4. Access Token update design

We can store the obtained results in Redis either on the first request or on subsequent updates to the Access token and Refresh token:

private void saveGoogleOAuthResponse(GoogleOAuthResponse response) {
    // the refresh token might be null if the trying to refresh token
    if(response.getRefresh_token() ! =null) {
        redisHelper.setGoogleRefreshToken(response.getRefresh_token());
    }
    redisHelper.setGoogleAccessToken(response.getAccess_token());
    redisHelper.setGoogleAccessTokenRefreshTime((response.getExpires_in()-60) *1000+System.currentTimeMillis());
}
Copy the code

Here the expires_in is in seconds, and I’m also directly calculating the next expiration point. I would make a refresh token request to expire one minute before requesting product information.

5. Obtain product information

Access to product information using an interface as follows, the document address: developers.google.com/android-pub…

// base url: https://androidpublisher.googleapis.com/
public interface GooglePlayPaymentApi {

    @GET("/androidpublisher/v3/applications/{packageName}/purchases/products/{productId}/tokens/{token}")
    Call<GoogleProductResponse> getProduct(@Path("packageName") String packageName,
                                           @Path("productId") String productId,
                                           @Path("token") String token,
                                           @Query("access_token") String accessToken);

    @GET("/androidpublisher/v2/applications/{packageName}/purchases/subscriptions/{subscriptionId}/tokens/{token}")
    Call<GoogleSubResponse> getSubscription(@Path("packageName") String packageName,
                                            @Path("subscriptionId") String subscriptionId,
                                            @Path("token") String token,
                                            @Query("access_token") String accessToken);
}
Copy the code

These two interfaces are used to get product information and subscription information respectively.

2.2 Overall purchase process design

As mentioned above, the logic on the client side is insecure and unreliable. In my app, the entire Google Pay process is as follows:

  • Complete the Google payment, callback, get purchaseToken
  • Access the backend verification interface, which records the order information and returns the verification result
  • If the server verification succeeds, the client invokes the Google Clearing libraryconsumeAsync()acknowledgePurchase()The purchase can be confirmed, and then the user completes the payment
  • If the purchase is confirmed to be successful, the backend is called to complete the payment interface, and the backend verifies the order information again. If the verification is successful, authorization is carried out
  • In addition, the most important server starts the scheduled task, scans the outstanding Google order at the specified time interval, pulls the order information from Google again, performs the authorization logic if the payment has been completed but not authorized, and ends the entire order process if the refund logic has been followed. The main purpose of this paper is to solve the problem that the successful response is not received after confirmation of purchase, and the problem of order loss caused by failure to call the back-end payment interface.

It should be noted that we will check and record the order information before confirming the payment. Of course, you can also directly confirm and then use the Google Clearing library queryPurchases() method to query the order history each time to solve the problem. This reduces one request to Google (Google daily requests are capped). In addition, it was important to note that the complete order information (package name, product ID, purchaseToken) was recorded at checkout so that the scheduled task could access Google’s order interface again.

Finally, deployment. Google’s interface requires overseas servers to work, and they are slightly more expensive than domestic servers. For my application, considering the initial stage of the project, only foreign Google order requests go to the overseas server, and other requests go to the domestic server. Database and Redis or stored in the previous server above, so that the database management is much easier, and do not need to rebuild the database and Redis environment, a lightweight server can basically meet the requirements.

2.3 a little pit

1. The request does not return the Refresh Token

Note that if we want to obtain the refresh token, we need to set the access_type parameter offline when concatenating the link, and return the refresh token only for the first authorization. Reference: stackoverflow.com/questions/1…

3, summarize

Google settlement integration of domestic data is relatively few, I still spent a lot of my time, here record the whole development and design process, to do backup and for later reference.

As for Yanyan, this is my newly developed note-taking software, which supports Markdown syntax and adopts file directory structure for notes. Notes can be synchronized between mobile phones and computers through WebDAV. The file structure has been arranged internally, and it can be edited and browseen in both ends without any changes. It also supports Hexo’s notepad and tag management, which is ideal for programmers.

Thanks for reading!