# Implementing ONE store IAP SDK

## Overview

The ONE store purchase library provides the latest features in a Java environment.&#x20;

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.

{% hint style="info" %}
In Android Studio (version: bumblebee) or later, add it to the `settings.gradle` file.
{% endhint %}

```gradle
repositories {
    ... 
    maven { url 'https://repo.onestore.net/repository/onestore-sdk-public' }
}
```

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

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

### \<queries> Setting

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

{% hint style="danger" %}
If you don’t set the **\<queries>** tag, the SDK won’t be able to find the ONE store service.
{% endhint %}

```xml
<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.net/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>
```

### **Setting Developer Options for Store Selection**

#### v21.04.00 Update - Added ONE Billing Lab Selection Feature <a href="#id-04.-sdk-v21.03.00update-addedonebillinglabselectionfeature" id="id-04.-sdk-v21.03.00update-addedonebillinglabselectionfeature"></a>

you can designate the store app integrated with the SDK by setting the `android:value` of `onestore:dev_option` as follows:

```xml
<manifest>
    <application>
        <meta-data android:name="onestore:dev_option" android:value="onestore_01" />
    </application>
</manifest>
```

<table><thead><tr><th width="226">android:value</th><th>Target Countries and Regions</th></tr></thead><tbody><tr><td><code>onestore_00</code></td><td>South Korea <em>(default)</em></td></tr><tr><td><code>onestore_01</code></td><td>Singapore, Taiwan</td></tr><tr><td><code>onestore_02</code></td><td>United States</td></tr><tr><td><code>onestore_03</code></td><td>ONE Billing Lab</td></tr></tbody></table>

{% hint style="info" %}
**Version History**

* **v21.04.00** : Added ONE Billing Lab &#x20;
* **v21.02.00** : With `android:value`, you can set South Korea, Singapore/Taiwan, and the United States.&#x20;
* **v21.01.00** : With `android:value`, only global can be set, and only Singapore/Taiwan store apps can be specified.
  {% endhint %}

{% hint style="danger" %}
**Note:** Be sure to remove this option from the binary for distribution versions.
{% endhint %}

## 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.](https://developer.android.com/reference/android/util/Log#summary)

```kotlin
/**
 * 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     |

{% hint style="danger" %}
For the distribution build version, remove this option as it may be a security risk.
{% endhint %}

### Error Handling <a href="#id-04.-sdk" id="id-04.-sdk"></a>

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.

### Log in to ONE store <a href="#id-04.-sdk" id="id-04.-sdk"></a>

#### Initializin&#x67;**`GaaSignInClient`**

`GaaSignInClient` is a library for log in to ONE store.

You create an instance using `getClient()` .

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
val signInClient = GaaSignInClient.getClient(activity)
```

{% endtab %}

{% tab title="Java" %}

```java
GaaSignInClient signInClient = GaaSignInClient.getClient(activity);
```

{% endtab %}
{% endtabs %}

#### Background login

Background login can be called via `silentSignIn()`.

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.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
signInClient.silentSignIn { signInResult ->
  
}
```

{% endtab %}

{% tab title="Java" %}

```java
signInClient.silentSignIn(new OnAuthListener() {
    @Override
    public void onResponse(@NonNull SignInResult signInResult) {
        
    }
});
```

{% endtab %}
{% endtabs %}

#### Foreground Login

Unlike `silentSignIn()`, 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.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
signInClient.launchSignInFlow(activity) { signInResult ->
  
}
```

{% endtab %}

{% tab title="Java" %}

```java
signInClient.launchSignInFlow(activity, new OnAuthListener() {
    @Override
    public void onResponse(@NonNull SignInResult signInResult) {
        
    }
});
```

{% endtab %}
{% endtabs %}

### Initializing PurchaseClient <a href="#id-04.-sdk-purchaseclient" id="id-04.-sdk-purchaseclient"></a>

`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 checked in the 'License Management' menu.

{% hint style="success" %}
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.
{% endhint %}

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
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()
```

{% endtab %}

{% tab title="Java" %}

```java
private PurchasesUpdatedListener listener = new PurchasesUpdatedListener() {
     @Override
     public void onPurchasesUpdated(IapResult iapResult, List<PurchaseData> purchases) {
         // To be implemented in a later section.
     }
};

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

{% endtab %}
{% endtabs %}

### ONE store Service Connection Settings <a href="#id-04.-sdk" id="id-04.-sdk"></a>

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.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
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.
    }
})
```

{% endtab %}

{% tab title="Java" %}

```java
purchaseClient.startConnection(new PurchaseClientStateListener() {
    @Override
    public void onSetupFinished(IapResult iapResult) {
        if (iapResult.isSuccess()) {
            // The PurchaseClient is ready. You can query purchases here.
        }
    }

    @Override
    public void onServiceDisconnected() {
        // Try to restart the connection on the next request to
        // PurchaseClient by calling the startConnection() method.
    }
});
```

{% endtab %}
{% endtabs %}

### Searching for Detailed Product Information <a href="#id-04.-sdk" id="id-04.-sdk"></a>

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()`.

`ProductType`is as follows:

<table><thead><tr><th width="246">Product</th><th>Enum</th></tr></thead><tbody><tr><td>Managed Product</td><td><code>ProductType.INAPP</code></td></tr><tr><td>Subscription Product</td><td><code>ProductType.SUBS</code></td></tr><tr><td>Monthly Subscription Product</td><td><del><code>ProductType.AUTO</code></del> (This product will not be supported in the future.)</td></tr></tbody></table>

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

{% hint style="warning" %}
ProductType.ALL can only be used for searching detailed product information, not for initiating purchase requests or retrieving purchase history.
{% endhint %}

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

{% tabs %}
{% tab title="Kotlin" %}

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

{% endtab %}

{% tab title="Java" %}
{% code fullWidth="false" %}

```java
ProductDetailsParams params = ProductDetailsParams.newBuilder()
        .setProductIdList(productIdList)
        .setProductType(ProductType.INAPP)
        .build();

purchaseClient.queryProductDetailsAsync(params, new ProductDetailsListener() {
    @Override
    public void onProductDetailsResponse(IapResult iapResult, List<ProductDetail> productDetails) {
        // Process the result. 
    }
});
```

{% endcode %}
{% endtab %}
{% endtabs %}

### Initiating Purchase Request <a href="#id-04.-sdk" id="id-04.-sdk"></a>

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.Builder`class 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.

{% hint style="success" %}
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.
{% endhint %}

{% hint style="warning" %}
`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.
{% endhint %}

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
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)
```

{% endtab %}

{% tab title="Java" %}

```java
Activity activity = ...

PurchaseFlowParams 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);
```

{% endtab %}
{% endtabs %}

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

<figure><img src="https://2218522982-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FgStyyzRzNh9x2u93ZH03%2Fuploads%2FgfQ7WLnx9P8ygTc922y1%2Fimage.png?alt=media&#x26;token=5b4232e6-d44f-461f-84c5-ba500455e085" alt=""><figcaption><p>[Figure 1] </p></figcaption></figure>

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.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
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 == ResponseCode.NEED_LOGIN) {
        // PurchaseClient by calling the launchLoginFlow() method.
    } else {
        // Handle any other error codes.
    }
}
```

{% endtab %}

{% tab title="Java" %}

```java
@Override
public void onPurchasesUpdated(IapResult iapResult, List<PurchaseData> purchases) {
    if (iapResult.isSuccess() && purchases != null) {
        for (PurchaseData purchase : purchases) {
            handlePurchase(purchase);
        }
    } else if (iapResult.getResponseCode() == ResponseCode.NEED_UPDATE) {
        // PurchaseClient by calling the launchUpdateOrInstallFlow() method.
    } else if (iapResult.getResponseCode() == ResponseCode.NEED_LOGIN) {
        // PurchaseClient by calling the launchLoginFlow() method.
    } else {
        // Handle any other error codes.
    }
}
```

{% endtab %}
{% endtabs %}

<figure><img src="https://2218522982-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FgStyyzRzNh9x2u93ZH03%2Fuploads%2FsPy90B6QhD39RVr8ekHb%2Fimage.png?alt=media&#x26;token=4d17a326-dbf9-4b23-96a2-fbcd83cbc310" alt=""><figcaption></figcaption></figure>

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 <a href="#id-04.-sdk" id="id-04.-sdk"></a>

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. <a href="#id-04.-sdk" id="id-04.-sdk"></a>

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

<figure><img src="https://2218522982-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FgStyyzRzNh9x2u93ZH03%2Fuploads%2FDyfONhDImsmjziCKunsj%2Fimage.png?alt=media&#x26;token=b2ed77a5-8a62-4922-8572-96f85d4fb589" alt=""><figcaption></figcaption></figure>

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

| Proration Mode                          | Description                                                                                                                                                                   |
| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 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 <a href="#id-04.-sdk" id="id-04.-sdk"></a>

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.

{% tabs %}
{% tab title="Kotlin" %}

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

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

purchaseClient.launchPurchaseFlow(activity, purchaseFlowParams)
```

{% endtab %}

{% tab title="Java" %}

```java
SubscriptionUpdateParams subscriptionUpdateParams = SubscriptionUpdateParams.newBuilder()
      .setProrationMode(desiredProrationMode)
      .setOldPurchaseToken(oldPurchaseToken)
      .build();

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

purchaseClient.launchPurchaseFlow(activity, purchaseFlowParams);
```

{% endtab %}
{% endtabs %}

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  <a href="#id-04.-sdk" id="id-04.-sdk"></a>

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 href="#id-04.-sdk-consume" id="id-04.-sdk-consume"></a>

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.

{% hint style="info" %}
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.
{% endhint %}

{% hint style="danger" %}
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.
{% endhint %}

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
// PurchaseData refers to purchase history retrieved via the PurchaseClient#queryPurchasesAsync method or the PurchasesUpdatedListener.
fun handlePurchase(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.
    }
}
```

{% endtab %}

{% tab title="Java" %}

```java
// PurchaseData refers to purchase history retrieved via the PurchaseClient#queryPurchasesAsync method or the PurchasesUpdatedListener.
private void handlePurchase(PurchaseData purchase) {     
    // Verify the purchase.
    // Ensure entitlement was not already granted for this purchaseToken.
    // Grant entitlement to the user.

    ConsumeParams consumeParams = ConsumeParams.newBuilder()
                                        .setPurchaseData(purchase)
                                        .build();
                                        
    purchaseClient.consumeAsync(consumeParams, new ConsumeListener() {
        @Override
        public void onConsumeResponse(IapResult iapResult, PurchaseData purchaseData) {
             // Process the result.
        }
    });
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
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.
{% endhint %}

#### Purchase Acknowledgement <a href="#id-04.-sdk-acknowledge" id="id-04.-sdk-acknowledge"></a>

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.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
// Purchase retrieved from PurchaseClient#queryPurchasesAsync or your PurchasesUpdatedListener.
fun handlePurchase(purchase: PurchaseData) {
    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.
            }
        }
    }
}
```

{% endtab %}

{% tab title="Java" %}

```java
// Purchase retrieved from PurchaseClient#queryPurchasesAsync or your PurchasesUpdatedListener.
private void handlePurchase(PurchaseData purchase) {
    if (purchase.getPurchaseState() == PurchaseState.PURCHASED) {
        if (!purchase.isAcknowledged()) {
            AcknowledgeParams acknowledgeParams = AcknowledgeParams.newBuilder()
                                                        .setPurchaseData(purchase)
                                                        .build();
                                                        
            purchaseClient.acknowledgeAsync(acknowledgeParams, new AcknowledgeListener() {
                @Override
                public void onAcknowledgeResponse(IapResult iapResult, PurchaseData purchaseData) {
                    // PurchaseClient by calling the queryPurchasesAsync() method.
                }
            });
        }
    }
}
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
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.
{% endhint %}

### Retrieving Purchase History <a href="#id-04.-sdk" id="id-04.-sdk"></a>

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.&#x20;

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`.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
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 == ResponseCode.NEED_LOGIN) {
        // PurchaseClient by calling the launchLoginFlow() method.
    } else {
        // Handle any other error codes.
    }
}
purchaseClient.queryPurchasesAsync(ProductType.INAPP, queryPurchasesListener)
```

{% endtab %}

{% tab title="Java" %}

```java
QueryPurchasesListener queryPurchasesListener = new QueryPurchasesListener() {
    @Override
    public void onPurchasesResponse(IapResult iapResult, List<PurchaseData> purchases) { 
        if (iapResult.isSuccess() && purchases != null) {
            for (PurchaseData purchase : purchases) {
                handlePurchase(purchase)
            }
        } else if (iapResult.getResponseCode() == ResponseCode.NEED_UPDATE) {
            // PurchaseClient by calling the launchUpdateOrInstallFlow() method.
        } else if (iapResult.getResponseCode() == ResponseCode.NEED_LOGIN) {
            // PurchaseClient by calling the launchLoginFlow() method.
        } else {
             // Handle any other error codes.
        }
    }
};
purchaseClient.queryPurchasesAsync(ProductType.INAPP, queryPurchasesListener);
```

{% endtab %}
{% endtabs %}

### Changing Monthly Subscription Product Status (Deprecated) <a href="#id-04.-sdk-deprecated" id="id-04.-sdk-deprecated"></a>

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.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
// Manage recurring product based on current recurring state.
// Only ONE action (CANCEL or REACTIVATE) must be applied at a time.
// NON_AUTO_PRODUCT is not eligible for recurring management.
fun manageRecurring(purchase: PurchaseData) {

    // Resolve the appropriate action from the current recurring state.
    val action = resolveRecurringAction(purchase.recurringState)
        ?: return // Skip if the product is not a recurring product

    val params = RecurringProductParams.newBuilder()
        .setPurchaseData(purchase)
        .setRecurringAction(action) // Only a single action is allowed
        .build()

    purchaseClient.manageRecurringProductAsync(params) { iapResult, purchaseData, action ->
        // Handle the result of the recurring action.
        // You can verify the updated purchase state via queryPurchasesAsync().
    }
}

/**
 * Maps RecurringState to a valid RecurringAction.
 *
 * RECURRING (0)  -> CANCEL
 * CANCEL (1)     -> REACTIVATE
 * NON_AUTO_PRODUCT (-1) -> not supported (returns null)
 *
 * @param state current recurring state of the purchase
 * @return corresponding RecurringAction or null if not applicable
 */
fun resolveRecurringAction(state: Int): RecurringAction? {
    return when (state) {
        RecurringState.RECURRING -> RecurringAction.CANCEL
        RecurringState.CANCEL -> RecurringAction.REACTIVATE
        RecurringState.NON_AUTO_PRODUCT -> null // Not a recurring product
        else -> null // Unknown state (safely ignored)
    }
}
```

{% endtab %}

{% tab title="Java" %}

```java
// Manage recurring product based on current recurring state.
// Only ONE action (CANCEL or REACTIVATE) must be applied at a time.
// NON_AUTO_PRODUCT is not eligible for recurring management.
public void manageRecurring(PurchaseData purchase) {

    // Resolve the appropriate action from the current recurring state.
    RecurringAction action = resolveRecurringAction(purchase.getRecurringState());

    if (action == null) {
        // Skip if the product is not a recurring product or state is invalid
        return;
    }

    RecurringProductParams params = RecurringProductParams.newBuilder()
            .setPurchaseData(purchase)
            .setRecurringAction(action) // Only a single action is allowed
            .build();

    purchaseClient.manageRecurringProductAsync(params, (iapResult, purchaseData, resultAction) -> {
        // Handle the result of the recurring action.
        // You can verify the updated purchase state via queryPurchasesAsync().
    });
}

/**
 * Maps RecurringState to a valid RecurringAction.
 *
 * RECURRING (0)  -> CANCEL
 * CANCEL (1)     -> REACTIVATE
 * NON_AUTO_PRODUCT (-1) -> not supported (returns null)
 *
 * @param state current recurring state of the purchase
 * @return corresponding RecurringAction or null if not applicable
 */
@Nullable
private RecurringAction resolveRecurringAction(int state) {
    switch (state) {
        case RecurringState.RECURRING:
            return RecurringAction.CANCEL;

        case RecurringState.CANCEL:
            return RecurringAction.REACTIVATE;

        case RecurringState.NON_AUTO_PRODUCT:
            return null; // Not a recurring product

        default:
            return null; // Unknown state (safely ignored)
    }
}
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
`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.
{% endhint %}

### Opening Subscription Management Screen <a href="#id-04.-sdk" id="id-04.-sdk"></a>

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.

{% tabs %}
{% tab title="Kotlin" %}

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

{% endtab %}

{% tab title="Java" %}

```java
public void launchManageSubscription(@Nullable PurchaseData purchaseData) {
    SubscriptionParams subscriptionParams = null;
    if (purchaseData != null) {
        subscriptionParams = SubscriptionParams.newBuilder()
            .setPurchaseData(purchaseData)
            .build();
    }
    purchaseClient.launchManageSubscription(mActivity, subscriptionParams);
}
```

{% endtab %}
{% endtabs %}

### Obtaining Market Distinction Code <a href="#id-04.-sdk" id="id-04.-sdk"></a>

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

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

{% tabs %}
{% tab title="Kotlin" %}

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

{% endtab %}

{% tab title="Java" %}

```java
purchaseClient.getStoreInfoAsync(new StoreInfoListener() {
    @Override
    public void onStoreInfoResponse(IapResult iapResult, String storeCode) {
        // Save storecode and use it in Server to Server API.
    }
});
```

{% endtab %}
{% endtabs %}

### **StoreEnvironment API Feature Addition**

The `StoreEnvironment.getStoreType()` API provides functionality to determine whether an application with the SDK has been installed via ONE Store.

#### **Store Type Definition**

The API returns a `StoreType` and has one of the following four values:

<table><thead><tr><th width="240">StoreType</th><th width="71">value</th><th>description</th></tr></thead><tbody><tr><td><code>StoreType.UNKNOWN</code></td><td>0</td><td>Unable to determine the app installation store (e.g., direct APK installation, unknown source).</td></tr><tr><td><code>StoreType.ONESTORE</code></td><td>1</td><td>Installed from ONE Store (or Developer Option is activated).</td></tr><tr><td><code>StoreType.VENDING</code></td><td>2</td><td>Installed from Google Play Store.</td></tr><tr><td><code>StoreType.ETC</code></td><td>3</td><td>Installed from a store other than ONE Store or Google Play.</td></tr></tbody></table>

#### **How to Use the API**

This API can be utilized by invoking `StoreEnvironment.getStoreType()`.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
import com.gaa.sdk.base.StoreEnvironment

val storeType = StoreEnvironment.getStoreType()

when (storeType) {
    StoreType.ONESTORE -> println("This app was installed from ONE Store.")
    StoreType.VENDING -> println("This app was installed from the Google Play Store.")
    StoreType.ETC -> println("This app was installed from a store other than ONE Store or Google Play.")
    StoreType.UNKNOWN -> println("The store information is unknown.")
}
```

{% endtab %}

{% tab title="Java" %}

```java
import com.gaa.sdk.base.StoreEnvironment;

int storeType = StoreEnvironment.getStoreType();

switch (storeType) {
    case StoreType.ONESTORE:
        System.out.println("ONE Store에서 설치된 앱입니다.");
        break;
    case StoreType.VENDING:
        System.out.println("Google Play Store에서 설치된 앱입니다.");
        break;
    case StoreType.ETC:
        System.out.println("기타 스토어에서 설치된 앱입니다.");
        break;
    case StoreType.UNKNOWN:
    default:
        System.out.println("스토어 정보를 알 수 없습니다.");
        break;
}
```

{% endtab %}
{% endtabs %}

#### **Store Determination Criteria**

This API determines the store through three methods:

* **Distributed via ONE Store market signature**\
  Determines if the app was installed from ONE Store by verifying distribution through ONE Store's market signature.
* **Based on Installer Package Name**\
  If not distributed via ONE Store's market signature, it uses the `PackageManager.getInstallerPackageName()` API to verify the store used during app installation.
* **When Developer Option (`onestore:dev_option`) is activated**\
  If `onestore:dev_option` is set, it always responds with `StoreType.ONESTORE`.

#### **Examples of Use Cases**

* **Applying UI Differentiation by Store**
  * If payment systems provided by ONE Store differ from those of other app markets, you can configure different UIs..

```kotlin
if (StoreEnvironment.getStoreType() == StoreType.ONESTORE) {
    showOneStorePaymentUI()
} else {
    showDefaultPaymentUI()
}
```

* **Restricting Features by Store**
  * You can set specific features to be available only on ONE Store.

```kotlin
if (StoreEnvironment.getStoreType() != StoreType.ONESTORE) {
    println("This feature is available only on ONE Store.")
    return
}
enableOneStoreExclusiveFeature()
```

### Installing ONE store Service <a href="#id-04.-sdk" id="id-04.-sdk"></a>

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.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
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.
    }
}
```

{% endtab %}
{% endtabs %}
