Several months ago, Sony rolled out their all-new PlayStation Plus service, which is home to a wealth of popular classic games. Its official blog wrote that its games catalog "will continue to refresh and evolve over time, so there is always something new to play."
I was totally on board with the idea and so… I thought why not build a lightweight mobile game together with my friends and launch it on a niche app store as a pilot. I did just this. The multiplayer survival game draws on a dark cartoon style and users need to utilize their strategic skills to survive. The game launch was all about sharing ideas, among English users specifically, but it attracted many players from non-English speaking countries like China and Germany. What a surprise!
Like many other game developers, I tried to achieve monetization through in-app user purchases. The app offers many in-game props, such as fancy clothes and accessories, weapons, and skill cards, to deliver a more immersive experience or to help users survive. This posed a significant challenge — as users are based in a number of different countries or regions, the app needs to show product information in the language of the country or region where the user's account is located, as well as the currency. How to do this?
Below is a walkthrough of how I implemented the language and currency localization function and the product purchase function for my app. I turned to HMS Core In-App Purchases (IAP) because it is very accessible. I hope this will help you.
Development Procedure
Product Management
Creating In-App Products
I signed in to AppGallery Connect to enable the IAP service and set relevant parameters first. After configuring the key event notification recipient address for the service, I could create products by selecting my app and going to Operate > Products > Product Management.
IAP supports three types of products, that is, consumables, non-consumables, and subscriptions. For consumables that are depleted as they are used and are repurchasable, I created products including in-game currencies (coins or gems) and items (clothes and accessories). For non-consumables that are purchased once and will never expire, I created products that unlock special game levels or characters for my app. For subscriptions, I went with products such as a monthly game membership to charge users on a recurring basis until they decide to cancel them.
Aside from selecting the product type, I also needed to set the product ID, name, language, and price, and fill in the product description. Voilà. That's how I created the in-app products.
Global Adaptation of Product Information
Here's a good thing about IAP: developers don't need to manage multiple app versions for users from different countries or regions!
All I have to do is complete the multilingual settings of the products in AppGallery Connect. First, select the product languages based on the countries/regions the product is available in. Let's say English and Chinese, in this case. Then, fill in the product information in these two languages. The effect is roughly like this:
Language | English | Chinese |
Product name | Stealth skill card | 隐身技能卡 |
Product description | Helps a user to be invisible so that they can outsurvive their enemies. | 帮助用户在紧急情况下隐身,打败敌人。 |
Now it's time to set the product price. I only need to set the price for one country/region and then IAP will automatically adjust the local price based on the exchange rate.
After the price is set, go to the product list page and click Activate. And that's it. The product has been adapted to different locations.
Purchase Implementation
Checking Support for IAP
Before using this kit, send an isEnvReady request to HMS Core (APK) to check whether my HUAWEI ID is located in the country/region where IAP is available. According to the kit's development documentation:
If the request result is successful, my app will obtain an IsEnvReadyResult instance, indicating that the kit is supported in my location.
If the request fails, an exception object will be returned. When the object is IapApiException, use its getStatusCode method to obtain the result code of the request.
If the result code is OrderStatusCode.ORDER_HWID_NOT_LOGIN (no HUAWEI ID signed in), use the getStatus method of the IapApiException object to obtain a Status object, and use the startResolutionForResult method of Status to bring up the sign-in screen. Then, obtain the result in the onActivityResult method of Activity. Parse returnCode from the intent returned by onActivityResult. If the value of returnCode is OrderStatusCode.ORDER_STATE_SUCCESS, the country/region where the currently signed-in ID is located supports IAP. Otherwise, an exception occurs.
You guys can see my coding below.
// Obtain the Activity object.
final Activity activity = getActivity();
Task<IsEnvReadyResult> task = Iap.getIapClient(activity).isEnvReady();
task.addOnSuccessListener(new OnSuccessListener<IsEnvReadyResult>() {
@Override
public void onSuccess(IsEnvReadyResult result) {
// Obtain the execution result.
String carrierId = result.getCarrierId();
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
if (e instanceof IapApiException) {
IapApiException apiException = (IapApiException) e;
Status status = apiException.getStatus();
if (status.getStatusCode() == OrderStatusCode.ORDER_HWID_NOT_LOGIN) {
// HUAWEI ID is not signed in.
if (status.hasResolution()) {
try {
// 6666 is a constant.
// Open the sign-in screen returned.
status.startResolutionForResult(activity, 6666);
} catch (IntentSender.SendIntentException exp) {
}
}
} else if (status.getStatusCode() == OrderStatusCode.ORDER_ACCOUNT_AREA_NOT_SUPPORTED) {
// The current country/region does not support IAP.
}
} else {
// Other external errors.
}
}
});
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 6666) {
if (data != null) {
// Call the parseRespCodeFromIntent method to obtain the result.
int returnCode = IapClientHelper.parseRespCodeFromIntent(data);
// Use the parseCarrierIdFromIntent method to obtain the carrier ID returned by the API.
String carrierId = IapClientHelper.parseCarrierIdFromIntent(data);
}
}
}
Showing Products
To show products configured to users, call the obtainProductInfo API in the app to obtain product details.
1. Construct a ProductInfoReq object, send an obtainProductInfo request, and set callback listeners OnSuccessListener and OnFailureListener to receive the request result. Pass the product ID that has been defined and taken effect to the ProductInfoReq object, and specify priceType for a product.
2. If the request is successful, a ProductInfoResult object will be returned. Using the getProductInfoList method of this object, my app can obtain the list of ProductInfo objects. The list contains details of each product, including its price, name, and description, allowing users to see the info of the products that are available for purchase.
List<String> productIdList = new ArrayList<>();
// Only those products already configured can be queried.
productIdList.add("ConsumeProduct1001");
ProductInfoReq req = new ProductInfoReq();
// priceType: 0: consumable; 1: non-consumable; 2: subscription
req.setPriceType(0);
req.setProductIds(productIdList);
// Obtain the Activity object.
final Activity activity = getActivity();
// Call the obtainProductInfo API to obtain the details of the configured product.
Task<ProductInfoResult> task = Iap.getIapClient(activity).obtainProductInfo(req);
task.addOnSuccessListener(new OnSuccessListener<ProductInfoResult>() {
@Override
public void onSuccess(ProductInfoResult result) {
// Obtain the product details returned upon a successful API call.
List<ProductInfo> productList = result.getProductInfoList();
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
if (e instanceof IapApiException) {
IapApiException apiException = (IapApiException) e;
int returnCode = apiException.getStatusCode();
} else {
// Other external errors.
}
}
});
Initiating a Purchase
The app can send a purchase request by calling the createPurchaseIntent API.
1. Construct a PurchaseIntentReq object to send a createPurchaseIntent request. Pass the product ID that has been defined and taken effect to the PurchaseIntentReq object. If the request is successful, the app will receive a PurchaseIntentResult object, and its getStatus method will return a Status object. The app will display the checkout screen of IAP using the startResolutionForResult method of the Status object.
// Construct a PurchaseIntentReq object.
PurchaseIntentReq req = new PurchaseIntentReq();
// Only the products already configured can be purchased through the createPurchaseIntent API.
req.setProductId("CProduct1");
// priceType: 0: consumable; 1: non-consumable; 2: subscription
req.setPriceType(0);
req.setDeveloperPayload("test");
// Obtain the Activity object.
final Activity activity = getActivity();
// Call the createPurchaseIntent API to create a product order.
Task<PurchaseIntentResult> task = Iap.getIapClient(activity).createPurchaseIntent(req);
task.addOnSuccessListener(new OnSuccessListener<PurchaseIntentResult>() {
@Override
public void onSuccess(PurchaseIntentResult result) {
// Obtain the order creation result.
Status status = result.getStatus();
if (status.hasResolution()) {
try {
// 6666 is a constant.
// Open the checkout screen returned.
status.startResolutionForResult(activity, 6666);
} catch (IntentSender.SendIntentException exp) {
}
}
}
}).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.
}
}
});
After the app opens the checkout screen and the user completes the payment process (that is, successfully purchases a product or cancels the purchase), IAP will return the payment result to your app through onActivityResult. You can use the parsePurchaseResultInfoFromIntent method to obtain the PurchaseResultInfo object that contains the result information.
If the purchase is successful, obtain the purchase data InAppPurchaseData and its signature data from the PurchaseResultInfo object. Use the public key allocated by AppGallery Connect to verify the signature.
When a user purchases a consumable, if any of the following payment exceptions is returned, check whether the consumable was delivered.
Payment failure (OrderStatusCode.ORDER_STATE_FAILED).
A user has purchased the product (OrderStatusCode.ORDER_PRODUCT_OWNED).
The default code is returned (OrderStatusCode.ORDER_STATE_DEFAULT_CODE), as no specific code is available.
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 6666) {
if (data == null) {
Log.e("onActivityResult", "data is null");
return;
}
// Call the parsePurchaseResultInfoFromIntent method to parse the payment result.
PurchaseResultInfo purchaseResultInfo = Iap.getIapClient(this).parsePurchaseResultInfoFromIntent(data);
switch(purchaseResultInfo.getReturnCode()) {
case OrderStatusCode.ORDER_STATE_CANCEL:
// The user cancels the purchase.
break;
case OrderStatusCode.ORDER_STATE_FAILED:
case OrderStatusCode.ORDER_PRODUCT_OWNED:
// Check whether the delivery is successful.
break;
case OrderStatusCode.ORDER_STATE_SUCCESS:
// The payment is successful.
String inAppPurchaseData = purchaseResultInfo.getInAppPurchaseData();
String inAppPurchaseDataSignature = purchaseResultInfo.getInAppDataSignature();
// Verify the signature using your app's IAP public key.
// Start delivery if the verification is successful.
// Call the consumeOwnedPurchase API to consume the product after delivery if the product is a consumable.
break;
default:
break;
}
}
}
Confirming a Purchase
After a user pays for a purchase or subscription, the app checks whether the payment is successful based on the purchaseState field in InAppPurchaseData. If purchaseState is 0 (already paid), the app will deliver the purchased product or service to the user, then send a delivery confirmation request to IAP.
For a consumable, parse purchaseToken from InAppPurchaseData in JSON format to check the delivery status of the consumable.
After the consumable is successfully delivered and its purchaseToken is obtained, your app needs to use the consumeOwnedPurchase API to consume the product and instruct the IAP server to update the delivery status of the consumable. purchaseToken is passed in the API call request. If the consumption is successful, the IAP server will reset the product status to available for purchase. Then the user can buy it again.
// Construct a ConsumeOwnedPurchaseReq object.
ConsumeOwnedPurchaseReq req = new ConsumeOwnedPurchaseReq();
String purchaseToken = "";
try {
// Obtain purchaseToken from InAppPurchaseData.
InAppPurchaseData inAppPurchaseDataBean = new InAppPurchaseData(inAppPurchaseData);
purchaseToken = inAppPurchaseDataBean.getPurchaseToken();
} catch (JSONException e) {
}
req.setPurchaseToken(purchaseToken);
// Obtain the Activity object.
final Activity activity = getActivity();
// Call the consumeOwnedPurchase API to consume the product after delivery if the product is a consumable.
Task<ConsumeOwnedPurchaseResult> task = Iap.getIapClient(activity).consumeOwnedPurchase(req);
task.addOnSuccessListener(new OnSuccessListener<ConsumeOwnedPurchaseResult>() {
@Override
public void onSuccess(ConsumeOwnedPurchaseResult result) {
// Obtain the execution result.
}
}).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.
}
}
});
For a non-consumable, the IAP server returns the confirmed purchase data by default. After the purchase is successful, the user does not need to confirm the transaction, and the app delivers the product.
For a subscription, no acknowledgment is needed after a successful purchase. However, as long as the user is entitled to the subscription (that is, the value of InApppurchaseData.subIsvalid is true), the app should offer services.
Conclusion
It's a great feeling to make a game, and it's an even greater feeling when that game makes you money.
In this article, I shared my experience of building an in-app purchase function for my mobile survival game. To make it more suitable for a global market, I used some gimmicks from HMS Core In-App Purchases to configure product information in the language of the country or region where the user's account is located. In short, this streamlines the purchase journey for users wherever they are located.
Did I miss anything? I'm looking forward to hearing your ideas.