Subscriptions are mostly used for video, music, and reading apps. Once a user has purchased a subscription, it can be automatically renewed on a periodic basis.
The graph below shows the process for purchasing a subscription.
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
This guide tells you how to integrate HUAWEI In-App Purchases' (IAP) product management function so you can manage subscriptions in your apps. It will also provide solutions for problems that can occur during the integration.
Key development steps:
1. Create a subscription in the product management system (PMS) and add it to your app. You can do this by referring to Adding a Product.
Notes:
(1) Before you create a subscription, you need to create a subscription group to manage subscriptions of the same type.
(2) A new subscription in PMS will be automatically allocated to a default group, so you cannot choose a specific group for it. The subscription also cannot be deleted or re-created. Fixing such problems takes a great deal of effort, especially when the product, whose ID stays the same, is released on multiple platforms.
(3) When you add a subscription, its status will be set to Invalid by default. You need to activate it before you can start selling it. You can present invalid subscriptions in your app, but users will be unable to purchase them.
2. How the Code Works
a. Check whether the country or region of the signed-in HUAWEI ID is supported by HUAWEI IAP.
Code:
/**
* Check whether the country or region of the signed-in HUAWEI ID is included in the countries or regions
* supported by HUAWEI IAP.
* @param activity indicates the activity object that initiates a request.
*/
public static void isBillingSupported(final Activity activity) {
// Get the Activity instance that calls this API.
Task<IsEnvReadyResult> task = Iap.getIapClient(activity).isEnvReady();
task.addOnSuccessListener(new OnSuccessListener<IsEnvReadyResult>() {
@Override
public void onSuccess(IsEnvReadyResult 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();
if (status.getStatusCode() == OrderStatusCode.ORDER_HWID_NOT_LOGIN) {
// Not signed in.
if (status.hasResolution()) {
try {
// 3333 is an int constant defined by the developer.
status.startResolutionForResult(activity, 3333);
} catch (IntentSender.SendIntentException exp) {
}
}
} else if (status.getStatusCode() == OrderStatusCode.ORDER_ACCOUNT_AREA_NOT_SUPPORTED) {
// The current country or region is not supported by HUAWEI IAP.
}
}
}
});
}
Notes:
(1) If a user tries to purchase a subscription in your app without first signing in to their HUAWEI ID, an error code will be reported to this API. Set the API to display the sign-in page upon receiving this code.
(2) If a user tries to purchase a subscription of your app from a location where HUAWEI IAP payment is not supported, an error code will be reported to this API.
(3) It is recommended that the context of the main activity be used, because this allows you to directly display the sign-in page and process subsequent services after sign-in.
It is best to call this API before the subscription list is displayed to users.
b. Obtain in-app product details configured in AppGallery Connect.
Code:
//**
* Obtain in-app product details configured in AppGallery Connect.
* @param productIds ID list of products to be queried.
* Each product ID must exist and be unique in the current app.
* @param type In-app product type.
* The value contains: 0: consumable 1: non-consumable 2 auto-renewable subscription
*/
private void queryProducts(final Activity activity) {
// Pass in the productId list of products to be queried.
List<String> productIdList = new ArrayList<>();
// The product ID is the same as that set by a developer when configuring product information
// in AppGallery Connect.
productIdList.add("SubProduct1001");
ProductInfoReq req = new ProductInfoReq();
// priceType: 0: consumable; 1: non-consumable; 2: auto-renewable subscription
req.setPriceType(IapClient.PriceType.IN_APP_SUBSCRIPTION);
req.setProductIds(productIdList);
// Call the obtainProductInfo API.
Task<ProductInfoResult> task = Iap.getIapClient(activity).obtainProductInfo(req);
task.addOnSuccessListener(new OnSuccessListener<ProductInfoResult>() {
@Override
public void onSuccess(ProductInfoResult result) {
// Obtain the result.
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.
}
}
});
}
When calling this API, errors can occur if:
(1) The product type parameter does not match the product. For example, the value 0 (consumable) is passed to the parameter type for a subscription.
(2) More than 200 products are imported in one batch.
(3) The product ID is different from the one configured in the PMS.
(4) The product has not been activated.
c. Create orders for in-app products in the PMS.
Code:
/**
* Create orders for in-app products in the PMS.
*
* @param productId ID of the in-app product to be paid.
* The in-app product ID is the product ID you set during in-app product
* configuration in AppGallery Connect.
* @param type In-app product type.
* The value contains: 0: consumable 1: non-consumable 2 auto-renewable subscription
*/
private void buy(final Activity activity, String productId, int type) {
// Construct a PurchaseIntentReq object.
PurchaseIntentReq req = new PurchaseIntentReq();
// The product ID is the same as that set by a developer when configuring product information in AppGallery Connect.
req.setProductId("productId");
// In-app product type contains:
// priceType: 0: consumable; 1: non-consumable; 2: auto-renewable subscription
// type:IapClient.PriceType.IN_APP_SUBSCRIPTION
req.setPriceType(type);
req.setDeveloperPayload("developer define info");
// Call the createPurchaseIntent API.
Task<PurchaseIntentResult> task = Iap.getIapClient(activity).createPurchaseIntent(req);
task.addOnSuccessListener(new OnSuccessListener<PurchaseIntentResult>() {
@Override
public void onSuccess(PurchaseIntentResult result) {
// Obtain the payment result.
Status status = result.getStatus();
if (status.hasResolution()) {
try {
// 6666 is an int constant defined by the developer.
status.startResolutionForResult(activity,status.startResolutionForResult(activity, Constants.REQ_CODE_BUY));
} 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.
}
}
});
}
When calling this API, errors can occur if:
(1) The product is still invalid after it has been purchased.
(2) The payment page does not display. This can happen if the parameter or context is incorrect. We recommend you use the context of the main activity, because this will enable your app to provide services after the user has made a successful payment.
(3) The user has to sign the payment agreement because they are purchasing a subscription for the first time.
d. HUAWEI IAP returns the payment result to your app through the main activity's onActivityResult callback.
Code:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == Constants.REQ_CODE_LOGIN || requestCode == Constants.REQ_CODE_BUYWITHPRICE_CONTINUE) {
int returnCode = IapClientHelper.parseRespCodeFromIntent(data);
Log.i(TAG,"onActivityResult, returnCode: " + returnCode);
if (returnCode == OrderStatusCode.ORDER_STATE_SUCCESS) {
// If success is returned, you can call the buyWithPrice API again.
//buyWithPrice(productId);
} else if(returnCode == OrderStatusCode.ORDER_ACCOUNT_AREA_NOT_SUPPORTED){
Log.e(TAG,"This is unavailable in your country/region." );
} else {
Log.e(TAG,"user cancel login" );
}
return;
}
if (requestCode == Constants.REQ_CODE_BUY) {
if (data == null) {
Log.e("onActivityResult", "data is null");
return;
}
PurchaseResultInfo purchaseResultInfo = Iap.getIapClient(this).parsePurchaseResultInfoFromIntent(data);
switch(purchaseResultInfo.getReturnCode()) {
case OrderStatusCode.ORDER_STATE_CANCEL:
// The user cancels payment.
break;
case OrderStatusCode.ORDER_STATE_FAILED:
case OrderStatusCode.ORDER_PRODUCT_OWNED:
// Check if there exists undelivered products.
break;
case OrderStatusCode.ORDER_STATE_SUCCESS:
// The payment is successful.
String inAppPurchaseData = purchaseResultInfo.getInAppPurchaseData();
String inAppPurchaseDataSignature = purchaseResultInfo.getInAppDataSignature();
// Use the public key of your app to verify the signature.
// If the signature is correct, you can deliver your products.
// If the user purchased a consumable product, call the consumeOwnedPurchase
// API to consume it after successfully delivering the product.
break;
default:
break;
}
}
}
Notes:
(1) This API is used for payment results like payment cancellations, abnormal purchasing activity, payment successes, or when the user tries to purchase a product they have already purchased.
(2) If a user tries to purchase a subscription without first signing in to their HUAWEI ID, set this API to resume processing after they have signed in.
(3) If a user tries to purchase a subscription without having first signed the payment agreement, set this API to enable your app to continue the purchasing process after the user has signed the agreement.
e. Once a user has successfully purchased a subscription, verify the purchase on your app to check that the subscription is valid and provides its service.
Code:
/**
* Deliver products for the user .
*/
private void deliverProduct(final String inAppPurchaseDataStr, final String inAppPurchaseDataSignature) {
if (TextUtils.isEmpty(inAppPurchaseDataStr) || TextUtils.isEmpty(inAppPurchaseDataSignature)) {
Utils.showMessage(this, "purchase data is error");
return;
}
if (CipherUtil.doCheck(inAppPurchaseDataStr, inAppPurchaseDataSignature, Key.getPublicKey())) {
try {
InAppPurchaseData inAppPurchaseDataBean = new InAppPurchaseData(inAppPurchaseDataStr);
if(inAppPurchaseDataBean.isSubValid())
{
// Provide the user with the service.
}
} catch (JSONException e) {
Log.e(TAG, "delivery:" + e.getMessage());
}
} else {
Log.e(TAG, "delivery:" + getString(R.string.verify_signature_fail));
}
}
Notes:
(1) You need to generate the public key used for verifying signatures by going to Project Setting > In-App Purchases > Settings in AppGallery Connect.
(2) After a successful verification, for services (such as video app memberships) that are only available when they have been renewed in time, if InApppurchaseData.subIsvalid is true, you need to keep providing the services for the user.
(3) If possible, store the delivered products' purchaseToken on your app server. This can then be used as a credential when a user switches between subscriptions or accounts.
f. Redelivery. This mechanism reduces unsuccessful deliveries.
Generally, if an exception (such as a network error or process failure) occurs after the subscription has been paid for, your app will be unable to detect whether the payment was successful. This means the purchased product may not be delivered as expected.
However, with HUAWEI IAP, you can redeliver products.
Code:
/**
* Query information about all subscribed in-app products, including consumables, non-consumables,
* and auto-renewable subscriptions.
* If consumables are returned, the system needs to deliver them and calls the consumeOwnedPurchase
* API to consume the products.
* If non-consumables are returned, the in-app products do not need to be consumed.
* If subscriptions are returned, all existing subscription relationships of the user under the
* app are returned.
* @param mClient IapClient instance to call the obtainOwnedPurchases API.
* @param type In-app product type.
* The value contains: 0: consumable 1: non-consumable 2 auto-renewable subscription
*/
public static void obtainOwnedPurchases(IapClient mClient, final int type) {
Log.i(TAG, "call obtainOwnedPurchases");
String continuationToken = null;
// Construct a OwnedPurchasesReq object.
OwnedPurchasesReq ownedPurchasesReq = new OwnedPurchasesReq();
// In-app product type contains:
// priceType: 0: consumable; 1: non-consumable; 2: auto-renewable subscription
ownedPurchasesReq.setPriceType(2);
// Call the obtainOwnedPurchases API.
// Get the Activity instance that calls this API.
Task<OwnedPurchasesResult> task = mClient.obtainOwnedPurchases(ownedPurchasesReq);
task.addOnSuccessListener(new OnSuccessListener<OwnedPurchasesResult>() {
@Override
public void onSuccess(OwnedPurchasesResult result) {
// Obtain the execution result.
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 payment public key to verify the signature of the inAppPurchaseData.
deliverProduct(inAppPurchaseData,InAppSignature);
// The verification is successful.
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.
}
}
});
}
Notes:
(1) This mechanism works by either querying the IAP server for information about all valid, in-app product subscriptions, and then reporting this to your app server and providing services, or by providing services directly from the client.
(2) For services (such as video app memberships) that are only available when they have been renewed in time, if InApppurchaseData.subIsvalid is true, you need to keep providing the services for the user. For other custom services (such as magazine subscriptions), if you require the subscription renewal data for previous periods, you can query the receipts for all subscriptions through IapClient.obtainOwnedPurchaseRecord. The receipt format is the same as it is in InAppPurchaseData.
(3) If InApppurchaseData.purchaseState is set to 0, the subscription is in purchased state. The time segment from InApppurchaseData.purchaseTime to InApppurchaseData.expirationDate indicates a subscription's validity period.
(4) If a subscription is in purchased state and its validity period matches your app server's valid provisioning period, your app needs to provide the relevant services. For all subscriptions, your app determines whether to provide services in this way.
3. Key subscription event callback from the Huawei IAP Server
(1) Before you request callback, you need to sign in to AppGallery Connect and go to Project Setting > In-App Purchases > Settings to generate a public key and app server callback address.
(2) Phases of the subscription process:
Promotional period: The user enjoys a subscription for free or at a promotional price. This period will begin when the user successfully subscribes.
Renewal:
First renewal: The system renews the subscription at the standard price. During this period, the subscription is valid and the user is entitled to the service provided by the subscription.
Second and subsequent renewals: For each of these renewals, HUAWEI IAP renews the subscription by periodically deducting fees from the user's account. It obtains the fee and product status 10 days before the subscription expires. HUAWEI IAP will attempt to deduct fees from the user's account 24 hours before the subscription renews. If this fails, the lapse period will start. During this period, HUAWEI IAP will try to deduct fees once a day. It will stop trying when this period ends.
Expiring: The subscription is valid and services are available. However, because the user has canceled the subscription, no fee will be deducted for the next renewal.
Expired: The subscription has expired and services are no longer available. There are several possible causes for this: HUAWEI IAP was unable to deduct a fee for the last renewal; the user has canceled the subscription; the user has not agreed to a price increase; the fee deducted account is abnormal.
To-be-effective: The subscription has not taken effect, and the user cannot use the service provided by the subscription. This period occurs when a user switches between subscriptions in the same subscription group. For example, a user may have switched to a yearly subscription from a monthly one which is still valid, in which case the yearly subscription would be in the to-be-effective period.
Suspended: Users can decide on a suspension plan for a subscription which is under renewal on the subscription management screen of HMS Core (APK). Once this plan is completed, the subscription renewal will be suspended, but the suspension will not take effect and users can still use the services provided by the subscription. Once the subscription validity period has ended, the subscription renewal will be suspended, and the user will no longer be able to use the services. Then, after the suspension period ends, the subscription will be automatically renewed. If the renewal is successful, the subscription can be used. If the renewal fails, the subscription will expire. Users can adjust suspension plans at any time, and the maximum suspension period is three months. Users can cancel the suspension plan at any time to revert to the auto-renewal status and enjoy the services immediately.
Grace period (coming soon): If you enable a grace period for your app, it will activate when HUAWEI IAP is unable to deduct a fee at the end of the subscription validity period. During the grace period, the user can still use the subscription while the HUAWEI IAP server attempts to deduct fees. If the fee deduction is successful within the grace period, the renewal will go ahead as planned. If the fee deduction is successful once the grace period has passed, the renewal will be interrupted. Then, the renewal start time will be the actual fee deduction time.
Lapse period: If a subscription is not renewed (for example, the user proactively cancels a renewal) or the renewal fails after the grace period has passed, a subscription enters the lapse period. For a subscription in the lapse period, there is a 30-day retention period when services are unavailable but a user can still resume the subscription. During this period, the subscription can be considered as having expired.
Key subscription event callback returned to your app server:
POST /subscription/notify/address
Content-Type: application/json; charset=UTF-8
Accept: application/json
Content-Length: 2754
Code:
{
"statusUpdateNotification": "{\"environment\":\"Sandbox\",\"notificationType\":7,\"subscriptionId\":\"1581789719266.D40972AC.3089\",\"orderId\":\"1581789719266.148748E7.3089\",\"latestReceipt\":\"00000173741056a37eef310dff9c6a86fec57efafe318ae478e52d9c4261994d64c8f6fc8ea1abbdx5347.5.3089\",\"latestReceiptInfo\":\"{\\\"autoRenewing\\\":true,\\\"subIsvalid\\\":true,\\\"orderId\\\":\\\"1581789719266.148748E7.3089\\\",\\\"lastOrderId\\\":\\\"1581790584090.DDB1EE93.5610\\\",\\\"packageName\\\":\\\"com.example.hwinappdemo\\\",\\\"applicationId\\\":123456,\\\"productId\\\":\\\"monthly_subscription2\\\",\\\"kind\\\":2,\\\"productName\\\":\\\"monthly_subscription2\\\",\\\"productGroup\\\":\\\"185F2E6A04BB4299B82C2525D3D68EBE\\\",\\\"purchaseTime\\\":1582791622434,\\\"oriPurchaseTime\\\":1582790122434,\\\"purchaseState\\\":0,\\\"developerPayload\\\":\\\"test\\\",\\\"purchaseToken\\\":\\\"00230173741056a37eef310dff9c6a86fec57efafe318ae478e52d9c4261994d64c8f6fc8ea1abbdx5347.5.3089\\\",\\\"purchaseType\\\":0,\\\"currency\\\":\\\"TRY\\\",\\\"price\\\":2,\\\"country\\\":\\\"TR\\\",\\\"subscriptionId\\\":\\\"1581789719266.D40972AC.3089\\\",\\\"quantity\\\":1,\\\"daysLasted\\\":26,\\\"numOfPeriods\\\":6,\\\"numOfDiscount\\\":0,\\\"expirationDate\\\":1582791922434,\\\"retryFlag\\\":1,\\\"introductoryFlag\\\":0,\\\"trialFlag\\\":0,\\\"renewStatus\\\":1,\\\"cancelledSubKeepDays\\\":30,\\\"payOrderId\\\":\\\"SandBox_1581789719266.148748E7.3089\\\",\\\"payType\\\":\\\"0\\\",\\\"acknowledged\\\":1,\\\"cancelWay\\\":0,\\\"cancellationTime\\\":1582789943392}\",\"latestReceiptInfoSignature\":\"S94effXhWisNXNE+lFBPv5DHGixYDY+3aI8fNDA+/wGQA+gZEx76Vilf0OqmW7zWorenMp98ra98nO7zJwmoZIF7R8/ZEd0V7dWiMOU+iMkhfDpfjAmtkraj5tjTYJR+QIhrQQWIxXg1AhqmGBuTyBdy0TVrfsVoKfCnh5oFZ5H7UB7jZA3KJtIHqsziAFdPEP67oE9kf4eHNsYAw/xSl4wTPXgsRJINu6SmCWcUzZ2UNMN/zWVgiqYlc9CGu6jParOfYeEnUCspdyZrsdWkmh2IB3pXOV8JxVsinzkklPwjotU21Oja46mjW9E+WSEomSjk9dop/iA0SjbzuH8pSogu0UyeO9FelFf/ZsaVQZLLjCHBK6pQ3SuHD3amqRB0GP3lGKdD1z5N7N5rO8ngS+mmlPi1gZwj+FiXdScVbmX6fOpvP+PFhyOginZp84EenKYx9Z9asXH+cJxmw9usN95cL7gAFhk+pu19Ng0Ro+t0+DPuj+cTOzyhVy7FTcDM\",\"autoRenewStatus\":1,\"productId\":\"monthly_subscription2\",\"applicationId\":\"123456\"}",
"notifycationSignature": "lD8NiUCrH+TK7gWBJB6XP3ZG9L2/TZvAGPIvRetAus9ZrKLjIzeagGjfi9rW9f/wEctIrp2gtDZ+k9xEyFT9apAqEdp629pUmufKe8jWlj6ZrHxqABcBQ2+ZFcDQH2u1Vm8tsEsk0ApQ4Z/nY2GCpifU/L6ITqT/Gn5hEBm7aCBEzOzg+SOBlCxqMOehusgPQTss32Y+E7KAPFy3SzH7o4/mMWOBoqvhZQrs/t9hlS8oamnsm3MmT3fNkoV8W/H7ckZhA62bfryjO8B/RVmz69x4CbvuIXj5Uo459aaIX6wE/5gPEqnnj1QrzYd10arNk6u0/nPUoKllm1NJ56qH/i9SLX+fRVnRnhvM/6t01IXcfxOg5Nx4pT2R/UkOEdcZs7lRrnWnVY7VsMQVZAkxbPAe1eSyzhGnW7s0mr1VfdMmr56eQ3zxNeQ2KXzeVQkBCb4EiY0MB2swFMzsWlo/OMBrUjG7ojflq0Shww45bSOBNO0nlz9B/keFj17fUKFt"
}
Case study:
In this example, a social media app integrates HUAWEI IAP. Below, you can see the kinds of major problems which may arise during the integration process, when they might occur, and how they can be solved.
1. Is there a redelivery mechanism available for subscriptions?
Scenario: The redelivery mechanism should take effect after a successful payment, but sometimes the purchased product is not delivered as expected because an exception occurs. For example, your app may fail to receive or correctly handle notifications about key subscription events, or the process may be interrupted by an unexpected app exit caused by a program error.
Solution: When your app restarts, set it to call obtainOwnedPurchases(OwnedPurchasesReq ownedPurchasesReq) to query information about all the auto-renewal subscriptions the user has purchased, and return the results. This information can be obtained from InAppPurchaseData and returned to your app server. It ensures your app can provide users with uninterrupted services. The redelivery mechanism can be implemented for all subscriptions using this kind of API.
2. When does the parameter subscriptionId change? Will it change when a user switches between subscriptions, or when a user resumes a subscription?
Scenario: This parameter is used to provide service credentials to users whom you want to be able to enjoy your service.
Solution: The subscription ID of a product remains the same when the subscription renewal is normal.
Currently, the subscription ID only changes in the following two scenarios:
l A user purchases a new product.
l A user resumes a subscription after the grace period. If this happens, the subscription ID changes because the user's previous subscription has become invalid. However, if a user proactively resumes a subscription before it enters the lapse period, the subscription ID will not change.
This is also true of the subscription's purchaseToken.
3. What notifications about key subscription events indicate the subscription is valid?
Scenario: Your app needs to be able to deliver a product to the user based on the notification types it receives.
Solution:
(1) For details about the notification types, refer to the Table above.
(2) The app will provide users with the services they are entitled once it has checked that InApppurchaseData.subIsvalid is set to true and the period from InApppurchaseData.purchaseTime to InApppurchaseData.expirationDate indicates that the subscription is valid.
4. What should I do with the subscriptions when my app supports multiple accounts for the same user?
Scenario 1: Your app has its own account system. For example, a user has two accounts, A and B. If this user purchases a monthly subscription for A, will this subscription be available for B?
Solution: HUAWEI IAP only distinguishes between HUAWEI IDs, and not user accounts registered to your app. Therefore, the purchased subscription is shared by all accounts as long as they are related to the same HUAWEI ID. In this scenario, we recommend that the user still be entitled to the service when signing in with B, up until the subscription they purchased for A has expired. Since the subscription ID stays the same, you can build an agreement mechanism according to your needs.
Scenario 2: A user purchases a monthly subscription for account A and then a yearly subscription for account B. Which of these subscriptions should my app provide them with?
Solution: In this scenario, if the user switches between subscriptions using HUAWEI IAP's subscription management feature, you can use setDeveloperPayLoad("xxxx") to set user tags and provide account B with the service when the yearly subscription becomes valid. However, when the user switches between subscriptions using the subscription switching function, your app cannot tell which account switches the subscription. Therefore, your app will provide the yearly subscription to account A, which the user used first to purchase a subscription in your app.
Scenario 3: What happens when a user disables switching between accounts A and B, cancels a subscription of A, and resumes the subscription for B?
Solution: The subscription's service will be provided to whichever user account is bound to the subscription ID. In this scenario, since this ID remains unchanged, the service could be provided to either A or B. To avoid this issue, your app server needs to unbind the subscription from A and provide the service to B.
Very interesting and helpful
Hmm
Very useful post.
Related
What Does a Delivery Failure Mean?
A delivery failure refers to a situation where a user does not receive the item they have purchased.
A user purchases a virtual product or service within an app, but the user does not receive the purchased item due to an error (such as network exception or process termination) occurring in data synchronization between the app and the in-app purchases server.
Delivery failures can be damaging to both users and developers, and may lead to your app receiving a one-star rating, or users directly turning away from it. This may in turn have a knock-on effect on potential users, driving them away from your app before even giving it a chance. Clearly, this is something that needs to be avoided.
Common Way of Handling a Delivery Failure
The user claims that they have paid but did not receive the purchased item, and subsequently requests a refund. This will undoubtedly break the user's trust in your app.
Integrating HUAWEI IAP to Eliminate Delivery Failures
With HUAWEI IAP, you can completely eliminate the chance of a missing delivery occurring.
Your app calls the APIs of HUAWEI IAP to query order information from the HUAWEI IAP server. The server will return orders that are paid but not consumed to your app, which will then redeliver the products based on the query result, and tell the server to update the order status.
You can complete integration by following these instructions:
HUAWEI IAP: Purchase Process
For a purchased consumable, your app will call the consumption API to consume the product. If the API call fails, product delivery will fail. A non-consumable product or subscription service will not experience a delivery failure.
A typical consumable purchasing process:
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
1. A user initiates a purchase request from your app, which your app then sends to HMS Core (APK).
2. Request a delivery. Verify the signature of purchase data before delivering the requested product. 3. Deliver the product and send the purchase token to your app server. This token can be used to obtain the product delivery status so that you can determine whether to redeliver a product if its consumption fails due to delivery failure.
3. After a product is successfully delivered, call the consumeOwnedPurchaseAPI to consume the product and send a notification to the Huawei IAP server to update its delivery status. purchaseToken is passed in the API call request, and once consumption is complete, the Huawei IAP server resets the product status to available for purchase. The product can then be purchased again.
Besides the consumeOwnedPurchase API provided by the IAP client, your app can also use the API provided by the IAP server for product consumption. For details, please refer to Confirming the Purchase for the Order Service.
How HUAWEI IAP redelivers a product:
With HUAWEI IAP you can redeliver a consumable when a delivery failure occurs because of data synchronization errors (caused by network exception, process termination, or others) between your app and the IAP server. The following figure illustrates the redelivery process.
Your app needs to trigger a redelivery in the following scenarios:
The app launches.
Result code -1 (OrderStatusCode. ORDER_STATE_FAILED) is returned for a purchase request.
Result code 60051 (OrderStatusCode. ORDER_PRODUCT_OWNED) is returned for a purchase request.
Implement the redelivery function as follows:
1. Use obtainOwnedPurchases to obtain the purchase information about the purchased but undelivered consumable. Specify priceType as 0 in OwnedPurchasesReq.
If this API is successfully called, HUAWEI IAP will return an OwnedPurchasesResult object, which contains the purchase data and signature data of all purchased products that have not being delivered. Use the public key allocated by AppGallery Connect to verify the signature. For more information about the verification method, please refer to Verifying the Signature in the Returned Result.
Each purchase’s data is a character string in JSON format that contains the parameters listed in InAppPurchaseData. You need to parse the purchaseState field from the InAppPurchaseData character string. If purchaseState of a purchase is 0, the purchase is successful and delivery will be performed.
Code:
// 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.
Perform 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 Huawei IAP server to update the delivery status. Following consumption, the Huawei IAP server resets the product status to available for purchase, allowing the product to be purchased again.
For more details, please click:
HUAWEI IAP official website
HUAWEI IAP Development Guide
HUAWEI HMS Core Community
To learn more, please visit:
HUAWEI Developers official website
Development Guide
Reddit to join developer discussions
GitHub or Gitee to download the demo and sample code
Stack Overflow to solve integration problems
Follow our official account for the latest HMS Core-related news and updates.
Original Source
View attachment Featured image.jpg
As the start of the new academic year approaches, many college students will be leaving their parents to start college life. However, a lack of experience makes it easy for college students to become victims of electronic fraud such as phone scams.
The start of the new academic year is often a period of time that sees an uptick in phone scams, especially those targeting college students. Some scammers trick students to download and register an account on malicious financial apps embedded with viruses and Trojan horses or ones that imitate legitimate apps. With such malicious apps installed on students' phones, scammers are able to steal students' sensitive data, such as bank card numbers and passwords. Some scammers trick students, by offering them small gifts or coupons, to scan QR codes which then direct them to pages that ask users to enter their personal information, such as their phone number and address. Once a student has done this, they will receive a large number of fraudulent calls and junk SMS messages from then on. If the students scan QR codes linking to phishing websites, their personal data may be leaked and sold for malicious purposes. Some scammers even lie about offering students scholarships or grants, in order to trick them into visiting phishing websites and entering their bank account numbers and passwords, causing significant financial losses to such students.
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
To deal with the ever-changing tricks of fraudsters, an app needs to detect phishing websites, malicious apps, and other risks and remind users to be on the lookout for such risks with in-app tips, in order to keep users and their data safe. So, is there a one-stop service that can enhance app security from multiple dimensions? Fortunately, HMS Core Safety Detect can help developers quickly build security capabilities into their apps, and help vulnerable user groups such as college students safeguard their information and property.
The AppsCheck API in Safety Detect allows your app to obtain a list of malicious apps installed on a user's device. The API can identify 99% of malicious apps and detect unknown threats based on app behavior. Your app can then use this information to determine whether to restrict users from performing in-app payments and other sensitive operations.
The URLCheck API in Safety Detect checks whether an in-app URL is malicious. If the URL is determined to be malicious, the app can warn the user of the risk or block the URL.
Safety Detect also provides capabilities to check system integrity and detect fake users, helping developers quickly improve their app security. The integration process is straightforward, which I'll describe below.
Demo
Integration ProcedurePreparationsYou can follow the instructions here to prepare for the integration.
Using the AppsCheck APIYou can directly call getMaliciousAppsList of SafetyDetectClient to obtain a list of malicious apps. The sample code is as follows:
Code:
private void invokeGetMaliciousApps() {
SafetyDetectClient appsCheckClient = SafetyDetect.getClient(MainActivity.this);
Task task = appsCheckClient.getMaliciousAppsList();
task.addOnSuccessListener(new OnSuccessListener<MaliciousAppsListResp>() {
@Override
public void onSuccess(MaliciousAppsListResp maliciousAppsListResp) {
// Indicates that communication with the service was successful.
// Use resp.getMaliciousApps() to obtain a list of malicious apps.
List<MaliciousAppsData> appsDataList = maliciousAppsListResp.getMaliciousAppsList();
// Indicates that the list of malicious apps was successfully obtained.
if(maliciousAppsListResp.getRtnCode() == CommonCode.OK) {
if (appsDataList.isEmpty()) {
// Indicates that no known malicious apps were detected.
Log.i(TAG, "There are no known potentially malicious apps installed.");
} else {
Log.i(TAG, "Potentially malicious apps are installed!");
for (MaliciousAppsData maliciousApp : appsDataList) {
Log.i(TAG, "Information about a malicious app:");
// Use getApkPackageName() to obtain the APK name of the malicious app.
Log.i(TAG, "APK: " + maliciousApp.getApkPackageName());
// Use getApkSha256() to obtain the APK SHA-256 of the malicious app.
Log.i(TAG, "SHA-256: " + maliciousApp.getApkSha256());
// Use getApkCategory() to obtain the category of the malicious app.
// Categories are defined in AppsCheckConstants.
Log.i(TAG, "Category: " + maliciousApp.getApkCategory());
}
}
}else{
Log.e(TAG,"getMaliciousAppsList failed: "+maliciousAppsListResp.getErrorReason());
}
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
// An error occurred during communication with the service.
if (e instanceof ApiException) {
// An error with the HMS API contains some
// additional details.
ApiException apiException = (ApiException) e;
// You can retrieve the status code using the apiException.getStatusCode() method.
Log.e(TAG, "Error: " + SafetyDetectStatusCodes.getStatusCodeString(apiException.getStatusCode()) + ": " + apiException.getStatusMessage());
} else {
// A different, unknown type of error occurred.
Log.e(TAG, "ERROR: " + e.getMessage());
}
}
});
}
Using the URLCheck API1. Initialize the URLCheck API.
Before using the URLCheck API, you must call the initUrlCheck method to initialize the API. The sample code is as follows:
Code:
SafetyDetectClient client = SafetyDetect.getClient(getActivity());
client.initUrlCheck();
2. Request a URL check.
You can pass target threat types to the URLCheck API as parameters. The constants in the UrlCheckThreat class include the current supported threat types.
Code:
public class UrlCheckThreat {
// URLs of this type are marked as URLs of pages containing potentially malicious apps (such as home page tampering URLs, Trojan-infected URLs, and malicious app download URLs).
public static final int MALWARE = 1;
// URLs of this type are marked as phishing and spoofing URLs.
public static final int PHISHING = 3;
}
a. Initiate a URL check request.
The URL to be checked contains the protocol, host, and path but does not contain the query parameter. The sample code is as follows:
Code:
String url = "https://developer.huawei.com/consumer/cn/";
SafetyDetect.getClient(this).urlCheck(url, appId, UrlCheckThreat.MALWARE, UrlCheckThreat.PHISHING).addOnSuccessListener(this, new OnSuccessListener<UrlCheckResponse >(){
@Override
public void onSuccess(UrlCheckResponse urlResponse) {
if (urlResponse.getUrlCheckResponse().isEmpty()) {
// No threat exists.
} else {
// Threats exist.
}
}
}).addOnFailureListener(this, new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
// An error occurred during communication with the service.
if (e instanceof ApiException) {
// HMS Core (APK) error code and corresponding error description.
ApiException apiException = (ApiException) e;
Log.d(TAG, "Error: " + CommonStatusCodes.getStatusCodeString(apiException.getStatusCode()));
// Note: If the status code is SafetyDetectStatusCode.CHECK_WITHOUT_INIT,
// you did not call the initUrlCheck() method or you have initiated a URL check request before the call is completed.
// If an internal error occurs during the initialization, you need to call the initUrlCheck() method again to initialize the API.
} else {
// An unknown exception occurred.
Log.d(TAG, "Error: " + e.getMessage());
}
}
});
b. Call the getUrlCheckResponse method of the returned UrlCheckResponse object to obtain the URL check result.
The result contains List<UrlCheckThreat>, which includes the detected URL threat type. If the list is empty, no threat is detected. Otherwise, you can call getUrlCheckResult in UrlCheckThreat to obtain the specific threat code. The sample code is as follows:
Code:
final EditText testRes = getActivity().findViewById(R.id.fg_call_urlResult);
List<UrlCheckThreat> list = urlCheckResponse.getUrlCheckResponse();
if (list.isEmpty()) {
testRes.setText("ok");
}
else{
for (UrlCheckThreat threat : list) {
int type = threat.getUrlCheckResult();
}
}
3. Close the URL check session.
If your app does not need to call the URLCheck API anymore or will not need to for a while, you can call the shutdownUrlCheck method to close the URL check session and release relevant resources.
Code:
SafetyDetect.getClient(this).shutdownUrlCheck();
ConclusionElectronic fraud such as phone scams are constantly evolving and becoming more and more difficult to prevent, bringing great challenges to both developers and users. To combat such risks, developers must utilize technical means to identify phishing websites, malicious apps, and other risks, in order to safeguard users' personal information and property.
In this article, I demonstrated how HMS Core Safety Detect can be used to effectively combat electronic fraud. The whole integration process is straightforward and cost-efficient, and is a quick and effective way to build comprehensive security capabilities into an app.
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
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 ProcedureProduct ManagementCreating In-App ProductsI 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 InformationHere'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:
LanguageEnglishChineseProduct nameStealth skill card隐身技能卡Product descriptionHelps 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 ImplementationChecking Support for IAPBefore 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.
Code:
// 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 ProductsTo 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.
Code:
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 PurchaseThe 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.
Code:
// 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.
}
}
});
2. 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.
Code:
@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 PurchaseAfter 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.
Code:
// 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.
ConclusionIt'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.
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
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 TestingSandbox testing of IAP supports end-to-end testing without real payments for joint debugging.
Preparing for Sandbox TestingI 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 PaymentsI 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 RenewalThe 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 RedeliveryWhen 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.
Code:
// 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.
[HEADING=1]Conclusion[/HEADING]
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.
}
});
Mobile apps have significantly changed the way we live, bringing about greater convenience. With our mobiles we can easily book hotels online when we go sightseeing, buy train and flight tickets online for business trips, or just pay for a dinner using scan and pay.
There is rarely a one-app-fits-all approach of offering such services, so users have to switch back and forth between multiple apps. This also requires users to register and sign in to different apps, which is a trouble itself because users will need to complete complex registration process and repeatedly enter their account names and passwords.
In addition, as technology develops, a developer usually has multiple Android apps and app versions, such as the quick app and web app, for different platforms. If users have to repeatedly sign in to different apps or versions by the same developer, the churn rate will likely increase. What's more, the developer may need to even pay for sending SMS messages if users choose to sign in to their apps through SMS verification codes.
Is there anything the developer can do to streamline the sign-in process between different apps and platforms so that users do not need to enter their account names and passwords again and again?
Well fortunately, HMS Core Keyring makes this possible. Keyring is a Huawei service that offers credential management APIs for storing user credentials locally on users' Android phones and tablets and sharing the credentials between different apps and different platform versions of an app. Developers can call relevant APIs in their Android apps, web apps, or quick apps to use Keyring services, such as encrypt the sign-in credentials of users for local storage on user devices and share the credentials between different apps and platforms, thus creating a seamless sign-in experience for users across different apps and platforms. Besides, all credentials will be stored in Keyring regardless of which type of APIs developers are calling, to implement unified credential management and sharing.
In this article, I'll share how I used Keyring to manage and share sign-in credentials of users. I hope this will help you.
Advantages
First, I'd like to explain some advantages of Keyring.
Building a seamless sign-in experience
Your app can call Keyring APIs to obtain sign-in credentials stored on user devices, for easy sign-in.
Ensuring data security and reliability
Keyring encrypts sign-in credentials of users for local storage on user devices and synchronizes the credentials between devices via end-to-end encryption technology. The encrypted credentials cannot be decrypted on the cloud.
Reducing the churn rate during sign-in
Keyring can simplify the sign-in process for your apps, thus reducing the user churn rate.
Reducing the operations cost
With Keyring, you can reduce the operations cost, such as the expense for SMS messages used by users to sign in to your app.
Development Procedure
Next, let's look at how to integrate Keyring. Before getting started, you will need to make some preparations, such as register as a Huawei developer, generate and configure your signing certificate fingerprint in AppGallery Connect, and enable Keyring. You can click here to learn about the detailed preparation steps, which will not be introduced in this article.
After making necessary preparations, you can now start integrating the Keyring SDK. I'll detail the implementation steps in two scenarios.
User Sign-in Scenario
In this scenario, you need to follow the steps below to implement relevant logic.
1. Initialize the CredentialClient object in the onCreate method of your activity. Below is a code snippet example.
Code:
CredentialClient credentialClient = CredentialManager.getCredentialClient(this);
2. Check whether a credential is available. Below is a code snippet example.
Code:
List<AppIdentity> trustedAppList = new ArrayList<>();
trustedAppList.add(new AndroidAppIdentity("yourAppName", "yourAppPackageName", "yourAppCodeSigningCertHash"));
trustedAppList.add(new WebAppIdentity("youWebSiteName", "www.yourdomain.com"));
trustedAppList.add(new WebAppIdentity("youWebSiteName", "login.yourdomain.com"));
SharedCredentialFilter sharedCredentialFilter = SharedCredentialFilter.acceptTrustedApps(trustedAppList);
credentialClient.findCredential(sharedCredentialFilter, new CredentialCallback<List<Credential>>() {
@Override
public void onSuccess(List<Credential> credentials) {
if (credentials.isEmpty()) {
Toast.makeText(MainActivity.this, R.string.no_available_credential, Toast.LENGTH_SHORT).show();
} else {
for (Credential credential : credentials) {
}
}
}
@Override
public void onFailure(long errorCode, CharSequence description) {
Toast.makeText(MainActivity.this, R.string.query_credential_failed, Toast.LENGTH_SHORT).show();
}
});
3. Call the Credential.getContent method to obtain the credential content and obtain the result from CredentialCallback<T>. Below is a code snippet example.
Code:
private Credential mCredential;
// Obtained credential.
mCredential.getContent(new CredentialCallback<byte[]>() {
@Override
public void onSuccess(byte[] bytes) {
String hint = String.format(getResources().getString(R.string.get_password_ok),
new String(bytes));
Toast.makeText(MainActivity.this, hint, Toast.LENGTH_SHORT).show();
mResult.setText(new String(bytes));
}
@Override
public void onFailure(long l, CharSequence charSequence) {
Toast.makeText(MainActivity.this, R.string.get_password_failed,
Toast.LENGTH_SHORT).show();
mResult.setText(R.string.get_password_failed);
}
});
4. Call the credential saving API when a user enters a new credential, to save the credential. Below is a code snippet example.
Code:
AndroidAppIdentity app2 = new AndroidAppIdentity(sharedToAppName,
sharedToAppPackage, sharedToAppCertHash);
List<AppIdentity> sharedAppList = new ArrayList<>();
sharedAppList.add(app2);
Credential credential = new Credential(username, CredentialType.PASSWORD, userAuth,
password.getBytes());
credential.setDisplayName("user_niceday");
credential.setSharedWith(sharedAppList);
credential.setSyncable(true);
credentialClient.saveCredential(credential, new CredentialCallback<Void>() {
@Override
public void onSuccess(Void unused) {
Toast.makeText(MainActivity.this,
R.string.save_credential_ok,
Toast.LENGTH_SHORT).show();
}
@Override
public void onFailure(long errorCode, CharSequence description) {
Toast.makeText(MainActivity.this,
R.string.save_credential_failed + " " + errorCode + ":" + description,
Toast.LENGTH_SHORT).show();
}
});
User Sign-out Scenario
Similarly, follow the steps below to implement relevant logic.
1. Initialize the CredentialClient object in the onCreate method of your activity. Below is a code snippet example.
Code:
CredentialClient credentialClient = CredentialManager.getCredentialClient(this);
2. Check whether a credential is available. Below is a code snippet example.
Code:
List<AppIdentity> trustedAppList = new ArrayList<>();
trustedAppList.add(new AndroidAppIdentity("yourAppName", "yourAppPackageName", "yourAppCodeSigningCertHash"));
trustedAppList.add(new WebAppIdentity("youWebSiteName", "www.yourdomain.com"));
trustedAppList.add(new WebAppIdentity("youWebSiteName", "login.yourdomain.com"));
SharedCredentialFilter sharedCredentialFilter = SharedCredentialFilter.acceptTrustedApps(trustedAppList);
credentialClient.findCredential(sharedCredentialFilter, new CredentialCallback<List<Credential>>() {
@Override
public void onSuccess(List<Credential> credentials) {
if (credentials.isEmpty()) {
Toast.makeText(MainActivity.this, R.string.no_available_credential, Toast.LENGTH_SHORT).show();
} else {
for (Credential credential : credentials) {
// Further process the available credentials, including obtaining the credential information and content and deleting the credentials.
}
}
}
@Override
public void onFailure(long errorCode, CharSequence description) {
Toast.makeText(MainActivity.this, R.string.query_credential_failed, Toast.LENGTH_SHORT).show();
}
});
3. Call the deleteCredential method to delete the credential and obtain the result from CredentialCallback. Below is a code snippet example.
Code:
credentialClient.deleteCredential(credential, new CredentialCallback<Void>() {
@Override
public void onSuccess(Void unused) {
String hint = String.format(getResources().getString(R.string.delete_ok),
credential.getUsername());
Toast.makeText(MainActivity.this, hint, Toast.LENGTH_SHORT).show();
}
@Override
public void onFailure(long errorCode, CharSequence description) {
String hint = String.format(getResources().getString(R.string.delete_failed),
description);
Toast.makeText(MainActivity.this, hint, Toast.LENGTH_SHORT).show();
}
});
Keyring offers two modes for sharing credentials: sharing credentials using API parameters and sharing credentials using Digital Asset Links. I will detail the two modes below.
Sharing Credentials Using API Parameters
In this mode, when calling the saveCredential method to save credentials, you can call the setSharedWith method to set parameters of the Credential object, to implement credential sharing. A credential can be shared to a maximum of 128 apps.
The sample code is as follows:
Code:
AndroidAppIdentity app1 = new AndroidAppIdentity("your android app name",
"your android app package name", "3C:99:C3:....");
QuickAppIdentity app2 = new QuickAppIdentity("your quick app name",
"your quick app package name", "DC:99:C4:....");
List<AppIdentity> sharedAppList = new ArrayList<>(); // List of apps with the credential is shared.
sharedAppList.add(app1);
sharedAppList.add(app2);
Credential credential = new Credential("username", CredentialType.PASSWORD, true,
"password".getBytes());
credential.setSharedWith(sharedAppList); // Set the credential sharing relationship.
credentialClient.saveCredential(credential, new CredentialCallback<Void>() {
@Override
public void onSuccess(Void unused) {
Toast.makeText(MainActivity.this,
R.string.save_credential_ok,
Toast.LENGTH_SHORT).show();
}
@Override
public void onFailure(long errorCode, CharSequence description) {
Toast.makeText(MainActivity.this,
R.string.save_credential_failed + " " + errorCode + ":" + description,
Toast.LENGTH_SHORT).show();
}
});
Sharing Credentials Using Digital Asset Links
In this mode, you can add credential sharing relationships in the AndroidManifest.xml file of your Android app. The procedure is as follows:
1. Add the following content to the <application> element in the AndroidManifest.xml file:
Code:
<application>
<meta-data
android:name="asset_statements"
android:value="@string/asset_statements" />
</application>
2. Add the following content to the res\values\strings.xml file:
Code:
<string name="asset_statements">your digital asset links statements</string>
The Digital Asset Links statements are JSON strings comply with the Digital Asset Links protocol. The sample code is as follows:
Code:
[{
"relation": ["delegate_permission/common.get_login_creds"],
"target": {
"namespace": "web",
"site": "https://developer.huawei.com" // Set your website domain name.
}
},
{
"relation": ["delegate_permission/common.get_login_creds"],
"target": {
"namespace": "android_app",
"package_name": "your android app package name",
"sha256_cert_fingerprints": [
"F2:52:4D:..."
]
}
},
{
"relation": ["delegate_permission/common.get_login_creds"],
"target": {
"namespace": "quick_app",
"package_name": "your quick app package name",
"sha256_cert_fingerprints": [
"C3:68:9F:..."
]
}
}
]
The relation attribute has a fixed value of ["delegate_permission/common.get_login_creds"], indicating that the credential is shared with apps described in the target attribute.
And that's all for integrating Keyring. That was pretty straightforward, right? You can click here to find out more about Keyring and try it out.
Conclusion
More and more developers are prioritizing the need for a seamless sign-in experience to retain users and reduce the user churn rate. This is especially true for developers with multiple apps and app versions for different platforms, because it can help them share the user base of their different apps. There are many ways to achieve this. As I illustrated earlier in this article, my solution for doing so is to integrate Keyring, which turns out to be very effective. If you have similar demands, have a try at this service and you may be surprised.
Did I miss anything? Let me know in the comments section below.