Sandbox Testing and Product Redelivery, for In-App Purchases

Sandbox Testing and Product Redelivery, for In-App Purchases

Hey, guys! I'm still working on my mobile multiplayer survival game. In my article titled Build a Game That Features Local In-App Purchases, I shared my experience of configuring in-app product information in the language and currency of the country or region where the user's account is located, which streamlines the purchase journey for users and boosts monetization.

Some new challenges have arisen, though. When an in-app product is configured, I need to test its purchase process before it can be brought online. Hence, I need a virtual purchase environment that doesn't actually charge me real money. Sandbox testing it is.

Aside from this, network latency or abnormal process termination can sometimes cause data of the app and the in-app purchases server to be out of synchronization. In this case, my app won't deliver the virtual products users have just purchased. This same issue can be pretty tricky for many developers and operations personnel as we don't want to see a dreaded 1 star on the "About this app" screen of our app on app stores or users venting their anger about our apps on tech forums. Of course my app lets users request a refund by filing a ticket to start the process, but guess how they feel about the extra time they have to put into this?

So I wondered how to implement sandbox testing and ensure a successful product delivery for my app. That's where HMS Core In-App Purchases (IAP) comes to the rescue. I integrated its SDK to do the trick. Let's see how it works.

Sandbox Testing

Sandbox testing of IAP supports end-to-end testing without real payments for joint debugging.

Preparing for Sandbox Testing

I added a test account by going to Users and permissions > Sandbox > Test accounts. The test account needs to be a registered HUAWEI ID and will take effect between 30 minutes and an hour after it has been added.

As the app package I want to test hasn't been released in AppGallery Connect, its versionCode should exceed 0. For an app package once released in AppGallery Connect, the versionCode should be greater than that of the released one.

If you fail to access the sandbox when trying out the function, use the IapClient.isSandboxActivated (for Android) or HMSIAP.isSandboxActivated API (for HarmonyOS) in your app for troubleshooting.

Testing Non-Subscription Payments

I signed in with the test account and installed the app to be tested on my phone. When a request was initiated to purchase a one-time product (stealth skill card), IAP detected that I was a test user, so it skipped the payment step and displayed a message indicating that the payment was successful.

It was impressively smooth. The purchase process in the sandbox testing environment accurately reflected what would happen in reality. I noticed that the purchaseType field on the receipt generated in IAP had a value of 0, indicating that the purchase was a sandbox test record.

Let's try out a non-consumable product — the chance to unlock a special game character. In the sandbox testing environment, I purchased it and consumed it, and then I could purchase this character again.

Sandbox testing for a one-time product on a phone

Testing Subscription Renewal

The purchase process of subscriptions is similar to that of one-time products but subscriptions have more details to consider, such as the subscription renewal result (success or failure) and subscription period. Test subscriptions renew much faster than actual subscriptions. For example, the actual subscription period is 1 week, while the test subscription renews every 3 minutes.

Sandbox testing for a subscription on a phone

Sandbox testing helps me test new products before I launch them in my app.

Consumable Product Redelivery

When a user purchased a consumable such as a holiday costume, my app would call an API to consume it. However, if an exception occurred, the app would fail to determine whether the payment was successful, so the purchased product might not be delivered as expected.

Note: A non-consumable or subscription will not experience such a delivery failure because they don't need to be consumed.

I turned to IAP to implement consumable redelivery. The process is as follows.

Consumable Redelivery Process

Here's my development process.

1. Call obtainOwnedPurchases to obtain the purchase data of the consumable that has been purchased but not delivered. Specify priceType as 0 in OwnedPurchasesReq.

If this API is successfully called, IAP will return an OwnedPurchasesResult object, which contains the purchase data and signature data of all products purchased but not delivered. Use the public key allocated by AppGallery Connect to verify the signature.

The data of each purchase is a character string in JSON format and contains the parameters listed in InAppPurchaseData. Parse the purchaseState field from the InAppPurchaseData character string. If purchaseState of a purchase is 0, the purchase is successful. Deliver the required product for this purchase again.

// Construct an OwnedPurchasesReq object.
OwnedPurchasesReq ownedPurchasesReq = new OwnedPurchasesReq();
// priceType: 0: consumable; 1: non-consumable; 2: subscription
ownedPurchasesReq.setPriceType(0);
// Obtain the Activity object that calls the API.
final Activity activity = getActivity();
// Call the obtainOwnedPurchases API to obtain the order information about all consumable products that have been purchased but not delivered.
Task<OwnedPurchasesResult> task = Iap.getIapClient(activity).obtainOwnedPurchases(ownedPurchasesReq);
task.addOnSuccessListener(new OnSuccessListener<OwnedPurchasesResult>() {
    @Override
    public void onSuccess(OwnedPurchasesResult result) {
        // Obtain the execution result if the request is successful.
        if (result != null && result.getInAppPurchaseDataList() != null) {
            for (int i = 0; i < result.getInAppPurchaseDataList().size(); i++) {
                String inAppPurchaseData = result.getInAppPurchaseDataList().get(i);
                String inAppSignature = result.getInAppSignature().get(i);
                // Use the IAP public key to verify the signature of inAppPurchaseData.
                // Check the purchase status of each product if the verification is successful. When the payment has been made, deliver the required product. After a successful delivery, consume the product.
                try {
                    InAppPurchaseData inAppPurchaseDataBean = new InAppPurchaseData(inAppPurchaseData);
                    int purchaseState = inAppPurchaseDataBean.getPurchaseState();
                } catch (JSONException e) {
                }
            }
        }
    }
}).addOnFailureListener(new OnFailureListener() {
    @Override
    public void onFailure(Exception e) {
        if (e instanceof IapApiException) {
            IapApiException apiException = (IapApiException) e;
            Status status = apiException.getStatus();
            int returnCode = apiException.getStatusCode();
        } else {
            // Other external errors.
        }
    }
});

2. Call the consumeOwnedPurchase API to consume a delivered product.

Conduct a delivery confirmation for all products queried through the obtainOwnedPurchases API. If a product is already delivered, call the consumeOwnedPurchase API to consume the product and instruct the IAP server to update the delivery status. After the consumption is complete, the server resets the product status to available for purchase. Then the product can be purchased again.

Conclusion

A 1-star app rating is an unwelcome sight for any developer. For game developers in particular, one of the major barriers to their app achieving a 5-star rating is a failed virtual product delivery.

I integrated HMS Core In-App Purchases into my mobile game to implement the consumable redelivery function, so now my users can smoothly make in-app purchases. Furthermore, when I need to launch a new skill card in the game, I can perform tests without having to fork out real money thanks to the kit.

I hope this practice helps you guys tackle similar challenges. If you have any other tips about game development that you'd like to share, please leave a comment.