Skip to main content

In-App Purchase

Overview

Before you begin, please make sure you have completed the Getting Started tutorial.

Setup

MSDK provides multiple payment channels in separate modules.

You can choose from Google Play or Huawei AppGallery according to your requirements.

Setup dependency

Add the following code in build.gradle on the app-level, and replace $msdk_version with the actual MSDK version.

implementation "com.garena.sdk.android:payment-google:$msdk_version"

Setup manifest

Make sure you have declared applicationId in your AndroidManifest.xml file.

<meta-data
android:name="com.garena.sdk.applicationId"
android:value="YourAppId" />

Core Purchase Flow

Retrieve All Payment Items

Retrieve a list of all payment items, including promotional items, rebate items and app items.

By default, getChanneList only returns non-rebate items. If you already have fetched rebate options and have acquired a valid rebate ID, then it's possible to fetch only the items related to this ID. To do this, please pass the rebate ID by this method builder.setRebateId(id)

Additionally, builder.setRebateId(PaymentManager.ALL_ITEMS) can be used to fetch all items, both rebate and non-rebate, then you can perform your own filtering.

    PaymentInfoRequestParams.Builder paramsBuilder = new PaymentInfoRequestParams.Builder();

paramsBuilder.setLocalizeProductPrice(true or false);
paramsBuilder.setRebateId(PaymentManager.NON_REBATE_ITEMS);

PaymentManager.getChannelList(this, paramsBuilder.build(), result -> {
// handle channel list result
if(result.isSuccess()) {
PaymentChannelInfo info = result.unwrap();
LocalizeResult localizeResult = info.getLocalizeResult();
List<PaymentChannel> channels = info.getChannels(); // by right, this should be a list of length 1

if(channels.size() >= 1) {
PaymentChannel channel = channels.get(0);

List<Denomination> items = channel.getItems();

// render your UI with the items
} else {
// this won't happen
}

} else {
MSDKError error = result.getErrorInfo();
// handle error
}
});

Retrieve App Item

Retrieve of valid app items information. Game client should use this information to display the corresponding valid app items to users.

    PaymentManager.getAppItems(this, serverId, roleId, locale, itemIds, result ->{
if(result.isSuccess()) {
List<AppItem> appItems = result.unwrap();
// render your UI
} else {
MSDKError err = result.getErrorInfo();
}
});

Make Payment

After retrieving the payment options, you can initiate a payment request using the following code. The payment request will trigger the platform's native payment flow.

Important Note

Never use point amount in MSDK callbacks API when topup and redeem succeed. It is not safe if you use this field to add in-game cash balance in your game directly.

Please call the GOP server side API(/app/point/get_balance) to get the new in-game cash balance once you get successful result in MSDK callback API.

    // create params with the selected denomination. You should have retrieved the denomination list by step b.1
PurchaseRequestParams purchaseRequestParams = new PurchaseRequestParams.Builder(itemID).build();

// pass the request params to SDK
PaymentManager.purchase(this /* activity instance */, purchaseRequestParams, (result, extra) -> {
if (result.isSuccess()) {
TransactionInfo transaction = result.unwrap();

} else {
MSDKError error = result.getErrorInfo();
// handle error

// since v5.9, we can obtain the exact payment step that the failure occurs
int paymentStep = extra.getPaymentStep();

if(paymentStep < PaymentStep.RECEIVE_RESULTS) {
// the error occurs before google returning the payment results to us.
}
}
PaymentEligibility paymentEligibility = extra.getPaymentEligibility();
// paymentEligibility can be null, so we MUST do a null check
if(paymentEligibility != null) {
// handle eligibility info
}
});

Process Pending/Incomplete Transaction

In some cases, a user's purchase may be interrupted or fail to complete due to network issues, server errors, or other problems. To ensure users receive their purchased items and maintain a good user experience, MSDK provides methods to process these pending or incomplete transactions.

You should call these methods:

  • After successful user login
  • When receiving payment failure callbacks
  • When receiving goods distribution failure callbacks

This helps ensure any pending transactions are properly processed and users receive their purchased items.

Note that items appearing in the user's inventory consist of both those redeemed with a promotion code via Google Play, and those purchased by following the ordinary purchase flow but failed to be committed due to network or server issues. They can be distinguished by PurchasedItemInfo.isPromotion().

// replace PaymentInfoRequestParams with serverId and roleId
PaymentManager.scanPurchaseInventory(this /* activity instance */, serverId, roleId, result -> {

});

Rebate Card Purchase

Rebate cards allow users to purchase items at a discounted price. When a user purchases a rebate card, they can use it to get a discount on future purchases within a specified time period. The discount amount and validity period varies by rebate card.

Before showing rebate cards to users, you'll need to retrieve the current valid rebate options using the APIs below. Make sure to refresh the list after each purchase since a rebate card's validity can change.

Retrieve Rebate Cards

Important Notes

NEVER try to cache the rebate card data, because the result is changing dynamically.

    PaymentManager.getRebateOptions(this, serverId, roleId, locale, rebateIds, result -> {
if (result.isSuccess()) {
List<RebateOptionItem> rebateOptions = result.unwrap();
// render the rebate card UI
} else {
MSDKError err = result.getErrorInfo();
}
});

Make Payment

After retrieving the rebate options, you can initiate a payment flow to purchase a rebate item. The process is similar to making a regular in-app purchase, but with an additional rebate ID parameter.

When the purchase is successful, you'll receive a callback with the transaction details.

    // create params with the selected denomination
PurchaseRequestParams.Builder builder = new PurchaseRequestParams.Builder(itemId);
/* setup builder params*/
builder.setRebateId(rebateId);

// pass the request params to SDK
PaymentManager.purchase(this /* activity instance */, builder.build(), result -> {
if (result.isSuccess()) {
TransactionInfo transaction = result.unwrap();

} else {
MSDKError error = result.getErrorInfo();
// handle error
}
});

Redeem Rebate Cards

The redeem feature allows users to claim their rebate cards. You can either redeem a specific rebate card using its ID or redeem all available rebate cards at once.

Please consider the following scenarios:

  1. If redeem rebate card request is successful on GOP server, but MSDK does not receive the response (e.g due to network error), immediate subsequent attempts to call redeem rebate card will return error. If such scenario is encountered, it is recommended that game server should query the latest balance, which will include the previous redeem.
  2. For the completion callback upon successful redemption, please call GOP server API to get the latest balance, instead of manual balance calculation using the redeemed amount returned by the callback.

Redeem all rebate cards

    PaymentManager.redeemAll(this, serverId, roleId, result-> {
// handle result
});

Redeem a specific rebate card

    PaymentManager.redeem(this, rebateId, serverId, roleId, result -> {
if (result.isSuccess()) {
RedeemInfo redeemInfo = result.unwrap();

} else {
MSDKError err = result.getErrorInfo();
}
});

T-rex Event Purchase

Retrieve Event Configs

Retrieve a list of events information in the specified region.

    PaymentManager.loadEventConfigs(this /* activity instance*/, region, true, result->{
if(result.isSuccess()) {

} else {

}
});

Get Event Pricing

Retrieve a list of all payment items, including promotional items, rebate items and app items for events in the specified region.

    PurchaseRequestParams.Builder builder = new PurchaseRequestParams.Builder(itemId, eventId)


/* setup builder params*/

PaymentManager.getEventsPricing(this, params, result -> {

}

Make Event Payment

Buy an event related product with their respective product ID in region

    PurchaseRequestParams.Builder builder = new PurchaseRequestParams.Builder(itemId);
/* setup builder params*/
PaymentManager.payEventInit(this /* activity instance */, builder.build(), new PaymentManager.EventCallback() {
@Override
public void onInitResult(@NonNull Result<EventInitResult> result) {
// handle event init result
}

@Override
public void onResult(@NonNull Result<TransactionInfo> result, @NonNull PaymentExtraInfo extra) {
// handle purchase result
}
});

Other IAP Features

Payment Arbitrage Prevention

In order to prohibit specific currencies in some regions, we've provided this feature since v5.1.2 (google IAP).

The game side should pass the current user region when building the params instance. The game side's PM needs to complete currency and region configuration on the Garena Open Platform.

PurchaseRequestParams.Builder builder = new PurchaseRequestParams.Builder(itemId);
builder.setRegion(user region);

Daily Topup Limit

The Games can limit the daily in-app purchase amount now.

This feature relies on the "Payment Arbitrage" feature, so please make sure you've configured the "region" param as well.

This API only accepts integer type (in USD).

    // setup the topup limit and region when create purchaseRequestParams with the Builder

PurchaseRequestParams.Builder builder = new PurchaseRequestParams.Builder(itemId);
...
builder.setRegion(user region);
builder.setTopupLimit(your expected topup limit in USD); // if passes 20, means the user can only spend 20 USD per day.
Limitations

Currently, the "Redeem Promo Code", "Out-Of-App-Purchase" and "Multi-Quantity-Purchase" functions can exceed this limitation.

If your project needs to use the Daily Topup Limit feature, please ensure these functions are disabled from the Google Play console.

Custom Game Data

Starting from v5.12, MSDK supports passing custom game data from the mobile client to the game server through the [Game Server Callback Page(coming soon)] after the user completes a payment.

To use this feature, simply include the custom game data when initiating the payment flow.

Data Limit

Currently, the game data is restricted to a maximum length of 1024.

PurchaseRequestParams.Builder builder = new PurchaseRequestParams.Builder(itemID);

builder.setGameData(/*custom game data in string type*/);

PaymentManager.purchase(activity, builder.build(), listener);

Google IAP Default UI

MSDK provides a default payment UI implementation for Android to simplify integration. This feature is only available on Android.

Setup dependency

If you want to use the default UI, you need to import the UI module into your project. Please add the following code into build.gradle on the app-level and replace $msdk_version with the actual MSDK version.

    implementation "com.garena.sdk.android:payment-ui:$msdk_version"

Bring up the purchase page

Important Notes
  1. If the game (with the same appId) is going to be distributed to several countries, you can specify the user's locale. MSDK will use the default locale if the game doesn't specify.
  2. If the game is going to be distributed to the European Union, you SHOULD use the setOfferPersonalized(true or false) method to disclose to users that an item's price was personalized using automated decision-making. Details here

Android Image showing personalized offer disclosure in Play UI

When true, the Play UI includes the disclosure "This price has been customized for you". When false, the UI omits the disclosure.

    PaymentInfoRequestParams.Builder builder = new PaymentInfoRequestParams.Builder();
builder.setServerId(0);
builder.setRoleId(0);
builder.setItemIds(itemIds);
builder.setAppItemIds(appItemIds);
builder.setRebateIds(rebateIds);
builder.setLocalizeProductPrice(true or false);
builder.setVirtualCurrencyName("Diamond");

String virtualCurrencyName = "Diamond"; // will be displayed as the title of the purchase page

PaymentManager.showPurchasePage(this /* activity instance */, virtualCurrencyName, builder.build(), (result, extra) -> {
// handle purchase result
if(result.isSuccess()) {
TransactionInfo info = result.unwrap();
// handle result
} else {
MSDKError error = result.getErrorInfo();
// deal with error code and error message
Logger.print(error.toString()); // this line is just an example
}
// handle extra info
});

FAQ

If a user chooses to use cash payment methods in Google Play In-App Billing, is there any expiry time for each transaction?

Yes. Users must make payment with cash in 7 days, or the transaction will be canceled by Google Play.

After a user makes payment with cash, Garena Open Platform will need to attribute the purchased goods and acknowledge it to Google Play. Otherwise, Google Play will refund the user after 3 days.

If a guest account has a subscription and binds the guest account to a Google account, would the subscription be transferred as well?

Yes