Implementing ONE store IAP SDK

Overview

The ONE store purchase library provides the latest features in a Java environment.

This guide explains how to implement ONE store purchase library functions.

Project Setup

Adding Repositories and Dependencies

Register the ONE store maven address in your project's top-level gradle file.

In Android Studio (version: bumblebee) or later, add it to the settings.gradle file.

repositories {
    ... 
    maven { url 'https://repo.onestore.co.kr/repository/onestore-sdk-public' }
}

Add the ONE store purchase library dependency to your app's build.gradle file.

dependencies {
    ...
    def iap_lastest_version = "21.xx.xx"
    implementation "com.onestorecorp.sdk:sdk-iap:$iap_lastest_version"
}

<queries> Setting

You must set the in your AndroidManifest.xml file. For details, refer to the notice.

If you don’t set the <queries> tag, the SDK won’t be able to find the ONE store service.

<manifest>
    <!-- 
        if your binary use ONE store's In-app SDK,
        Please make sure to declare the following query on Androidmanifest.xml. 
        Refer to the notice for more information.
        https://dev.onestore.co.kr/devpoc/support/news/noticeView.omp?noticeId=32968
     -->
    <queries>
        <intent>
            <action android:name="com.onestore.ipc.iap.IapService.ACTION" />
        </intent>
        <intent>
            <action android:name="android.intent.action.VIEW" />
            <data android:scheme="onestore" />
        </intent>
    </queries>
    ...
    <application>
        ...
    </application>
</manifest>

Test Option Settings for Global Store Distribution

This option forces the SDK to integrate with the global store.

This feature has been implemented from SDK v21.01.00.

<manifest>
    ...
    <application>
        <activity>
            ...
        </activity>
        ...
        
        <!-- Options for in-app testing on your global store -->
        <meta-data android:name="onestore:dev_option" android:value="global" />
    </application>
</manifest>

In the distribution build version, this option must be removed.

Applying In-app Library

Setting Log Level

During development, you can set the log level to expose the flow of data in the SDK in more detail. This operates based on the values defined in android.util.Log.

/**
 * Set the log level.<br/>
 * {@link Log#VERBOSE}, {@link Log#DEBUG}, {@link Log#INFO},
 * {@link Log#WARN}, {@link Log#ERROR}
 * @param level int
 */
com.gaa.sdk.base.Logger.setLogLevel(2)
Constants Value Value

VERBOSE

2

DEBUG

3

INFO (default)

4

WARN

5

ERROR

6

For the distribution build version, remove this option as it may be a security risk.

Error Handling

The ONE store payment library returns errors in the form of IapResult. IapResult includes a ResponseCode which categorizes payment-related errors that can occur in the app. For instance, if error codes like RESULT_NEED_LOGIN or RESULT_NEED_UPDATE are received, the app must handle them appropriately.

Logging in to ONE store

InitializingGaaSignInClient

GaaSignInClient is a library for logging in to ONE store.

You create an instance using getClient() .

val signInClient = GaaSignInClient.getClient(activity)

Background Login

Background login can be called via slientSignIn().

If the user is already logged into a ONE store account, token login is attempted in the background thereafter. A response will be received in the form of a SignInResult object for success or failure.

signInClient.silentSignIn { signInResult ->
  
}

Foreground Login

Unlike slientSignIn(), this function must only be called from the UiThread.

It initially attempts background login but handles failures internally in the SDK.

Then, it displays a login screen to prompt the user to log in.

signInClient.launchSignInFlow(activity) { signInResult ->
  
}

Initializing PurchaseClient

PurchaseClient is the primary interface for communication between the ONE store payment library and the app.

To avoid multiple PurchasesUpdatedListener callbacks for a single event, it is recommended to keep the PurchaseClient connection open.

To create PurchaseClient, use newBuilder(). To receive updates related to purchases, call setListener() to pass a reference to the PurchasesUpdatedListener. This listener receives all purchase-related updates of the app. setBase64PublicKey()is used by the SDK to check the signature for tampering with purchase data. While it is optional, it is recommended.

The license key can be found in the ONE store developer center.

For security, it is advised to use the license key by receiving it through a server or other secure means, rather than storing it directly in the app code.

private val listener = PurchasesUpdatedListener { iapResult, purchases ->
       // To be implemented in a later section.
}

private var purchaseClient = PurchaseClient.newBuilder(activity)
   .setListener(listener)
   .setBase64PublicKey(/*your public key*/) // optional
   .build()

ONE store Service Connection Settings

After creating the PurchaseClient, you must connect it to the ONE store service.

To connect, call startConnection(). The connection process is asynchronous, and once client connection is completed, a callback will be received through the PurchaseClientStateListener.

The following example shows how to start the connection and test whether it is ready for use.

purchaseClient.startConnection(object : PurchaseClientStateListener {
    override fun onSetupFinished(iapResult: IapResult) {
        if (iapResult.isSuccess) {
            // The PurchaseClient is ready. You can query purchases here.
        }
    }

    override fun onServiceDisconnected() {
        // Try to restart the connection on the next request to
        // PurchaseClient by calling the startConnection() method.
    }
})

Searching for Detailed Product Information

To search for detailed product information, call queryProductDetailsAsync(). This is an important step before displaying products to users.

When calling queryProductDetailsAsync(), pass an instance of ProductDetailParams specifying a list of in-app product IDs created in the ONE store developer center, along with setProductType().

ProductTypeis as follows:

ProductEnum

Managed Product

ProductType.INAPP

Subscription Product

ProductType.SUBS

Monthly Subscription Product

ProductType.AUTO (This product will not be supported in the future.)

To search for all types of data at once, set ProductType.ALL.

ProductType.ALL can only be used for searching detailed product information, not for initiating purchase requests or retrieving purchase history.

To handle asynchronous task results, you must implement the ProductDetailsListener interface.

val params = ProductDetailsParams.newBuilder()
        .setProductIdList(productIdList)
        .setProductType(ProductType.INAPP)
        .build()
purchaseClient.queryProductDetailsAsync(params) { iapResult, productDetails -> 
    // Process the result.
}

Initiating Purchase Request

To make a purchase request from the app, call the launchPurchaseFlow() function on the main thread.

This function creates a PurchaseFlowParams object based on the values of the ProductDetail object, obtained by calling queryProductDetailsAsync(). Use the PurchaseFlowParams.Builderclass to create a PurchaseFlowParams object.

setDeveloperPayload() is an optional field with a maximum of 200 bytes, set by the developer. It can be used after payment to verify data integrity and for additional data. setProductName() can be used if you want to change and display the product name at the time of purchase. setQuantity()only applies to managed in-app products and is used when purchasing multiple units of a product.

ONE store offers various promotions to users such as discount coupons, cashback, etc. Developers can restrict or allow user participation in promotions using gameUserId and promotionApplicable parameters during a purchase request. Developers pass their app's unique user identification number and choice of promotion participation, and ONE store applies the user's promotional benefits based on these values.

gameUserId and promotionApplicable parameters are optional and should only be used after prior consultation with the ONE store business department. In general cases, these values are not sent. In addition, even when sending values after consultation, the gameUserId should be sent as a hashed unique value for privacy protection.

val activity: Activity = ...

val purchaseFlowParams = PurchaseFlowParams.newBuilder()
      .setProductId(productId)
      .setProductType(productType)
      .setDeveloperPayload(devPayload)    // optional
      .setQuantity(1)                     // optional
      .setProductName("")                 // optional
      .setGameUserId("")                  // optional
      .setPromotionApplicable(false)      // optional
      .build()

purchaseClient.launchPurchaseFlow(activity, purchaseFlowParams)

A successful call to launchPurchaseFlow() displays the following screen: [Figure 1] represents the regular payment purchase screen.

When a purchase is successful, the purchase operation result is sent to the onPurchasesUpdated() function of the PurchasesUpdatedListener interface. This listener is specified when initializing PurchaseClient via the setListener() function.

override fun onPurchasesUpdated(iapResult: IapResult, purchases: List<PurchaseData>?) {
    if (iapResult.isSuccess && purchases != null) {
        for (purchase in purchases) {
            handlePurchase(purchase)
        }
    } else if (iapResult.responseCode == ResponseCode.NEED_UPDATE) {
        // PurchaseClient by calling the launchUpdateOrInstallFlow() method.
    } else if (iapResult.responseCode == ReponseCode.NEED_LOGIN) {
        // PurchaseClient by calling the launchLoginFlow() method.
    } else {
        // Handle any other error codes.
    }
}

A purchase token, a unique identifier indicating the user and product ID, is also generated when a purchase is successful. While the purchase token can be stored within the app, it is safer to pass it to a backend server that can authenticate the purchase and protect against fraud. The purchase token for managed and regular payment products is issued each time a payment occurs. (For monthly subscription products, the purchase token remains the same while automatic payment is renewed.) In addition, users receive a transaction receipt via email that includes the receipt number. For managed products, an email is received each time a purchase is made, and for subscription and monthly subscription products, an email is received when first purchased and thereafter when renewed.

Subscription

Subscriptions renew automatically until canceled. Subscriptions can have the following statuses:

  • Active: User is in good standing and can access their subscription.

  • Schedule Pause: Users can select when to pause subscriptions during use.

    • Weekly Subscription: Can be paused at week 1 to 3.

    • Monthly Subscription: Can be paused at month 1 to 3.

    • Annual Subscription: Does not support pauses.

  • Scheduled Cancellation: Users can select when to cancel their subscription during use. Payment will not be made on the next payment date.

  • Grace & Hold: If the user encounters payment issues, the payment will not be made on the next payment date. Immediate "Subscription Cancellation" is possible without scheduling a cancellation.

This allows users to upgrade, downgrade, or change their subscription.

To upgrade or downgrade subscriptions, you can set a proration mode at the time of purchase or set how changes affect subscription users.

The following table shows available proration modes (PurchaseFlowParams.ProrationMode).

Proration ModeDescription

IMMEDIATE_WITH_TIME_PRORATION

Subscription replacement occurs immediately, and remaining time is adjusted based on the price difference, either credited or charged. (This is default behavior.)

IMMEDIATE_AND_CHARGE_PRORATED_PRICE

Subscription replacement occurs immediately, and billing cycle remains the same. The price for the remaining period is charged. (This option is only available for upgrades.)

IMMEDIATE_WITHOUT_PRORATION

Subscription replacement occurs immediately, and a new price is charged on the next payment date. The billing cycle remains the same.

DEFERRED

Replacement applies when existing plan expires, and the new fee is charged simultaneously.

Upgrade or Downgrade

Subscription can offer users upgrades or downgrades using the same API as when initiating purchase request. However, to apply an upgrade or downgrade to a subscription, the existing subscription purchase token and prorated mode value are required.

As in the following example, you must provide information about the current subscription, future (upgrade or downgrade) subscription, and proration mode.

val subscriptionUpdateParams = SubscriptionUpdateParams.newBuilder()
      .setProrationMode(desiredProrationMode)
      .setOldPurchaseToken(oldPurchaseToken)
      .build()

val purchaseFlowParams = PurchaseFlowParams.newBuilder()
      .setProductId(newProductId)
      .setProductType(productType)
      .setProductName(productName)        // optional
      .setDeveloperPayload(devPayload)    // optional
      .setSubscriptionUpdateParams(subscriptionUdpateParams)
      .build()

purchaseClient.launchPurchaseFlow(activity, purchaseFlowParams)

The response for an upgrade or downgrade is received in the PurchasesUpdatedListener, as with initiating purchase request logic. Responses can also be received from retrieving purchase history. Even when subscription is purchased in proration mode, purchase confirmation is needed using PurchaseClient.acknowledgeAsync()just like any other regular purchases.

Purchase Processing

Once a purchase is completed, the app must process the purchase confirmation. In most cases, apps receive purchase notifications through the PurchasesUpdatedListener. Or as described in retrieving purchase history, the app may process it by calling the PurchaseClient.queryPurchasesAsync() function.

You can confirm a purchase using one of the following methods:

  • For consumable products, use PurchaseClient.consumeAsync().

  • For non-consumable products, use PurchaseClient.acknowledgeAsync().

Consuming Managed Product

A managed product cannot be repurchased until consumed.

To consume a product, call consumeAsync(). You must also implement the ConsumeListener interface to receive results of consumption operation.

If a managed product is not consumed it can be used as a permanent product type, and if consumed immediately after purchase it can be used as a consumable product. Additionally, if consumed after a certain period, it can be used as a limited-time product.

If a purchase is not acknowledged or consumed within 3 days, it will be determined that the user hasn't received the product and an automatic refund will be issued.

fun handlePurchase(purchase: PurchaseData) {
    // Purchase retrieved from PurchaseClient#queryPurchasesAsync
    // or your PurchasesUpdatedListener.
    val purchase: PurchaseData = ...
      
    // Verify the purchase.
    // Ensure entitlement was not already granted for this purchaseToken.
    // Grant entitlement to the user.

    val consumeParams = ConsumeParams.newBuilder()
                            .setPurchaseData(purchase)
                            .build()
                            
    purchaseClient.consumeAsync(consumeParams) { iapResult, purchaseData -> 
        // Process the result.
    }
}

Since consumption requests can sometimes fail, it is necessary to check with a secure backend server to ensure that each purchase token has not been used. This prevents the app from granting multiple entitlements for the same purchase. Alternatively, you can wait until a successful consumption response had been received before granting entitlements.

Purchase Acknowledgement

To process confirmation of a non-consumable product, use the PurchaseClient.acknowledgeAsync()function. This can be used for managed products, monthly subscription products, and subscription products.

You can use the PurchaseData.isAcknowledged() function to determine if a purchase has been acknowledged. You must also implement the AcknowledgeListener interface to receive results of the acknowledgment operation.

// Purchase retrieved from PurchaseClient#queryPurchasesAsync or your PurchasesUpdatedListener.
fun handlePurchase(PurchaseData purchase) {
    if (purchase.purchaseState == PurchaseState.PURCHASED) {
        if (!purchase.isAcknowledged) {
            val acknowledgeParams = AcknowledgeParams.newBuilder()
                                        .setPurchaseData(purchase)
                                        .build()
                                        
            purchaseClient.acknowledgeAsync(acknowledgeParams) { iapResult, purchaseData ->
                 // PurchaseClient by calling the queryPurchasesAsync() method.
            }
        }
    }
}

The PurchaseData passed to the AcknowledgeListener.onAcknowledgeResponse() function is the data at the time of the request, so the acknowledgeState value does not change. You must replace it with updated data by retrieving purchase history.

Retrieving Purchase History

Using PurchasesUpdatedListener alone does not guarantee that all purchases have been processed. There are several scenarios where an app may fail to track or recognize a purchase.

In the following scenarios, the app may not receive a purchase response or fail to recognize a purchase.

  • Network Issues: If the network connection is lost before device receives the purchase notification through PurchasesUpdatedListener after a user successfully makes a purchase and is confirmed by ONE store.

  • Multiple Devices: When an item is purchased on one device, then switched to another device.

In these scenarios, the app may fail to receive the purchase response or recognize the purchase. To counter this, the app should call PurchaseClient.queryPurchasesAsync() in onCreate() or onResume() to verify that purchases have been successfully processed.

Callback processing is the same as with PurchasesUpdatedListener.

val queryPurchasesListener = QueryPurchasesListener { iapResult, purchases -> 
    if (iapResult.isSuccess && purchases != null) {
        for (purchase in purchases) {
            handlePurchase(purchase)
        }
    } else if (iapResult.responseCode == ResponseCode.NEED_UPDATE) {
        // PurchaseClient by calling the launchUpdateOrInstallFlow() method.
    } else if (iapResult.responseCode == ReponseCode.NEED_LOGIN) {
        // PurchaseClient by calling the launchLoginFlow() method.
    } else {
        // Handle any other error codes.
    }
}
purchaseClient.queryPurchasesAsync(ProductType.INAPP, queryPurchasesListener)

Changing Monthly Subscription Product Status (Deprecated)

A monthly subscription product renews on the same date of the following month after the initial purchase. The status of a monthly subscription product can be checked through PurchaseData.getRecurringState().

To change the status of a monthly subscription product, use PurchaseClient.manageRecurringProductAsync(). Input purchase data and the desired PurchaseClient.RecurringAction value into the RecurringProductParams object.

As of SDK V21 (API V7), creating new monthly subscription products is no longer possible. Use subscription products with a one-month payment cycle instead.

// Purchase retrieved from PurchaseClient#queryPurchasesAsync or your PurchasesUpdatedListener.
fun manageRecurring(purchase: PurchaseData) {
    val recurringParams = RecurringProductParams.newBuilder()
            .setPurchaseData(purchase)
            .setRecurringAction(RecurringAction.CANCEL | RecurringAction.REACTIVATE)
            .build()

    purchaseClient.manageRecurringProductAsync(recurringParams) { iapResult, purchaseData, action ->
        // PurchaseClient by calling the queryPurchasesAsync() method.
    }
}

PurchaseData passed as RecurringProductListener.onRecurringResponse() is the data at the time of request, so the recurringState value does not change. You must replace it with updated data by retrieving purchase history.

Opening Subscription Management Screen

You can display a screen for managing products with ongoing subscriptions.

By including PurchaseData in SubscriptionsParams as a parameter, purchase data is checked, and the management screen for that subscription product is launched. However, if you insert null in SubscriptionParams, the user's subscription list screen will be launched.

The following is an example of how to display the subscription management screen.

fun launchManageSubscription(@Nullable purchaseData: PurchaseData) {
    val subscriptionParams = when (purchaseData != null) {
        true -> SubscriptionParams.newBuilder()
                    .setPurchaseData(purchaseData)
                    .build()
        else -> null
    }
    purchaseClient.launchManageSubscription(mActivity, subscriptionParams)
}

Obtaining Market Distinction Code

From SDK v19 onward, a market distinction code is needed to use the Server API.

You can obtain a market distinction code through getStoreInfoAsync().

purchaseClient.getStoreInfoAsync { iapResult, storeCode ->
    // Save storecode and use it in Server to Server API.
}

Installing ONE store Service

In-app purchases cannot be used if the version of ONE store service is outdated or is not installed. This can be checked in IapResult.getResponseCode() when connecting through PurchaseClient.startConnection(). If RESULT_NEED_UPDATE occurs, you must call the launchUpdateOrInstallFlow() method.

val activity: Activity = ...

purchaseClient.launchUpdateOrInstallFlow(activity) { iapResult ->
    if (iapResult.isSuccess) {
        // If the installation is completed successfully,
        // you should try to reconnect with the ONE store service. 
        // PurchaseClient by calling the startConnection() method.
    }
}

Last updated