# 使用SDK實現ONE store In-App支付

## 概要

ONE store 支付程序庫（Library）在Java 環境提供最新功能。本指南對實現ONE store 支付程序庫（Library）功能的方法進行说明。

## 設置Project

### 添加倉庫（repository）及從屬項目

在Project最上位`gradle`文件上註冊 ONE store `maven`地址。

{% hint style="info" %}
Android Studio (version: bumblebee)以上的情况，在 settings.gradle文件中添加。
{% endhint %}

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

在App的`build.gradle`文件添加ONE store支付程序庫從屬項目。

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

### 設置 \<queries>

需要在 `AndroidManifest.xml`文件設置。詳細内容請參考公告事項。

{% hint style="danger" %}
如果不設&#x7F6E;**\<queries>** tag, 就無法在SDK找到ONE store服務。
{% 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>
```

### 選擇商店設置開發者選項 <a href="#undefined-3" id="undefined-3"></a>

#### v21.04.00更新 - ONE Billing Lab 選擇功能 <a href="#id04.sdk-zhui-jia-v21.03.00-geng-xin-onebillinglab-xuan-ze-gong-neng" id="id04.sdk-zhui-jia-v21.03.00-geng-xin-onebillinglab-xuan-ze-gong-neng"></a>

可以通過如下設置 onestore:dev\_option 的 android:value 值來指定與 SDK 連動的商店應用。\
應位於 `<application>` 標籤的直屬子級，並應添加以下元素。

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

<table><thead><tr><th>android:value</th><th>適用國家/地區</th><th data-hidden></th></tr></thead><tbody><tr><td><code>onestore_00</code></td><td>South Korea</td><td></td></tr><tr><td><code>onestore_01</code></td><td>Singapore, Taiwan</td><td></td></tr><tr><td><code>onestore_02</code></td><td>United States</td><td></td></tr><tr><td><code>onestore_03</code></td><td>ONE Billing Lab</td><td></td></tr></tbody></table>

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

* **v21.04.00** : ONE Billing Lab 選擇功能
* **v21.02.00** : 使用 `android:value` 可設定韓國、新加坡/台灣及美國。
* **v21.01.00** : 使用 `android:value` 僅能設定 global，且僅能指定新加坡/台灣商店應用程式。
  {% endhint %}

{% hint style="danger" %}
注意：在發佈版本的二進位檔中，務必移除此選項。
{% endhint %}

## 適用App内程序庫（in-app Library）

### 設置Log level

研發階段設置Log level，可以更仔细顯示SDK的數據流向。以 [`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)
```

| 常數             | 值 |
| -------------- | - |
| VERBOSE        | 2 |
| DEBUG          | 3 |
| INFO (default) | 4 |
| WARN           | 5 |
| ERROR          | 6 |

{% hint style="danger" %}
發行版本網絡安全比較弱，本選項需要刪除。
{% endhint %}

### 錯誤處理 <a href="#id-04.-sdk" id="id-04.-sdk"></a>

ONE store 支付程序庫以`IapResult`形式反饋錯誤。`IapResult`包含分類App内可能出現的支付相關錯誤的`ResponseCode`。例如，如果`IapResult`收到`RESULT_NEED_LOGIN` 或 `RESULT_NEED_UPDATE`等錯誤代碼（code），應用軟件需要做出相應的處理。

### 登錄ONE store <a href="#id-04.-sdk" id="id-04.-sdk"></a>

**`GaaSignInClient`** 初始化

`GaaSignInClient`是為了登錄ONE store的程序庫。\
通過`getClient()`生成instance。

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

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

{% endtab %}

{% tab title="Java" %}

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

{% endtab %}
{% endtabs %}

#### 登錄後台

通過`silentSignIn()`()調用後台登錄。

用戶已經登錄ONE store帳戶時，之後開始嘗試從後台token登錄。作為成功或失敗的结果值，以`SignInResult`對象獲取回覆。

{% 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 %}

#### 登錄前台

不同於`silentSignIn()`，僅在`UiThread` 調用相應函數。

雖然原則上首先嘗試後台登錄，但是對失敗的處理，在SDK將專門處理。

{% 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 %}

### PurchaseClient 初始化 <a href="#id-04.-sdk-purchaseclient" id="id-04.-sdk-purchaseclient"></a>

PurchaseClient是ONE store支付程序庫和App之間通信的基本接口。

為了避免生成單一活動相關的幾個`PurchasesUpdatedListener` 回朔的狀況，建議打開 `PurchaseClient`連接。

想要生成`PurchaseClient`，需要使用`newBuilder()`。想要收到購買相關信息，需要調用`setListener()`傳送對`PurchasesUpdatedListener`參考。這個Listener接收App的所有購買相關資訊。`setBase64PublicKey()`在SDK負責購買數據偽造篡改相關的簽字確認操作。雖然是選項值，但是建議使用。

可以在研发者中心確認許可證密鑰。<br>

<figure><img src="/files/qB7rwFYNt2qDkBNYGO4Y" alt=""><figcaption></figcaption></figure>

{% hint style="success" %}
許可證密鑰，為了確保安全，更推薦使用伺服器等接收傳送使用，而不是將其保存为APP内的代碼。
{% 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服務連接設置 <a href="#id-04.-sdk" id="id-04.-sdk"></a>

生成`PurchaseClient後`，需要連接ONE store服務。.

需要調用`startConnection()`進行連接。連接程序非同步，完成用戶端連接後，通過`PurchaseClientStateListener`收到回朔。

下面例子是測試連接開始、使用準備與否的方法。

{% 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 %}

### 查詢商品詳細資訊 <a href="#id-04.-sdk" id="id-04.-sdk"></a>

調用`queryProductDetailsAsync()`，查詢商品詳情。是给用戶標記產品前之重要階段。

調用`queryProductDetailsAsync()`時，同時與`setProductType()`一起傳送在ONE store研發者中心生成的指定内部app商品ID文字列目錄的`ProductDetailParams`的instance。

`ProductType`如下

<table><thead><tr><th width="246">Product</th><th>Enum</th></tr></thead><tbody><tr><td>管理型商品</td><td><code>ProductType.INAPP</code></td></tr><tr><td>定期支付商品 (訂閱商品)</td><td><code>ProductType.SUBS</code></td></tr><tr><td>包月型商品</td><td><del><code>ProductType.AUTO</code></del> (该商品今后本商品将不予支持) </td></tr></tbody></table>

想一次性查詢上述所有類型資訊，請設置`ProductType.ALL`。

{% hint style="warning" %}
ProductType.ALL僅可在查詢商品詳細資訊中使用，在請求購買，查詢購買明细中無法使用。
{% endhint %}

要處理非同步操作结果，需要實現`ProductDetailsListener` 接口。

{% 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 %}

### 請求購買 <a href="#id-04.-sdk" id="id-04.-sdk"></a>

為了在應用軟件請求購買，要在基本線程調用`launchPurchaseFlow()` 函數。

這一函數以調用`queryProductDetailsAsync()` 函數獲取的`ProductDetail`對象的值為基礎，生成`PurchaseFlowParams`對象。要生成`PurchaseFlowParams`對象，需要使用`PurchaseFlowParams.Builder` class。

`setDeveloperPayload()`作為研發者隨意輸入的值，最多200byte。此值可以用於確認支付後資訊的整合性和附加數據。\
`setProductName()`在想要支付時變更顯示商品名的情况下使用。`setQuantity()`僅用於管理型app内商品，在購買數個同一商品時使用。

{% hint style="success" %}
ONEstore正在進行為用户提供優惠券、現金卡等諸多優惠宣傳活動。

研發公司可以在請求購買時使用`gameUserId`, `promotionApplicable`參數，限制或允許app用户參與宣傳活動。\
研發公司選擇傳送app的固有用户識別號碼及是否參與宣傳活動，ONE store以此值為基礎，適用用戶的宣傳優惠。
{% endhint %}

{% hint style="warning" %}
`gameUserId`, `promotionApplicable`參數作為選項值，只能在事先與ONE store事業部負責人協商宣傳活動時使用，其他一般情况下不發送值。\
另外，即便是已經達成事先協議並發送值，為了保護個人資訊，`gameUserId`也需要以*hash*的固有值形式傳送。
{% 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 %}

如果成功調用`launchPurchaseFlow()`，顯示如下界面。【圖片1】顯示定期支付購買界面。

<figure><img src="/files/YnNSFzuDn0t9IiqjROie" alt=""><figcaption><p>【圖片1】</p></figcaption></figure>

如果購買成功，向`PurchasesUpdatedListener` 接口的 `onPurchasesUpdated()` 函數傳送購買操作结果。該Listener 在PurchaseClient 初始化時，使用`setListener()` 被指定。

{% 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="/files/YRnemAGrDzyRsJ7JeO7k" alt=""><figcaption></figcaption></figure>

如果購買成功，購買數據還會生成用戶及商品ID的固有識別符號購買token。購買token雖然可在應用軟件内儲存，但是最好將token傳送到驗證購買、避免詐騙的後端伺服器。\
管理型商品和定期支付商品的購買token在每次支付的时候都會發行購買token。

（包月型商品自动支付更新的时候，購買token將保持不變。）\
另外，用互通過電子郵件收到包含收據號碼的交易收據。管理型商品每次購買都會收到電子郵件，包月型商品和定期支付商品在第一次購買和之後有更新時會收到電子郵件。

### 定期支付 <a href="#id-04.-sdk" id="id-04.-sdk"></a>

定期支付在取消之前一直自動更新。定期支付可能出現如下狀態。

* 激活：用戶使用内容過程中沒有問題的良好狀態，可以訪問定期支付。
* 預約暫停：當用戶使用定期支付中想要暫停時，選擇此選項。
  * 周定期支付：以1-3周為單位進行暫停。
  * 月定期支付：以1-3個月為單位進行暫停。
  * 年定期支付：不予支持暫停。
* 預約解除：雖然用戶在使用定期支付，但是想取消時可以選擇。下一个支付日將不會支付。
* 延期、保留：如果用戶出現支付問題，則無法在下一个支付日支付。無法取消預約，可以立刻“解除訂閱”。

#### 允許用戶升级、降级或變更定期支付 <a href="#id-04.-sdk" id="id-04.-sdk"></a>

如果用戶想要升级或降级定期支付时，在購買時可設置比例分配模式，或者設定變更事項影響定期支付用戶的方式。

<figure><img src="/files/fTLs2uSGhRTiPA44vWyb" alt=""><figcaption></figcaption></figure>

下表列出了可使用的比例分配模式 (`PurchaseFlowParams.ProrationMode`)

| 比例分配模式                                  | 說明                                                   |
| --------------------------------------- | ---------------------------------------------------- |
| IMMEDIATE\_WITH\_TIME\_PRORATION        | 定期支付的變更將立即進行，剩餘時間基於差價條整後退款或支付。（這是基本操作）               |
| IMMEDIATE\_AND\_CHARGE\_PRORATED\_PRICE | 定期支付的變更將立即進行，請求支付周期和之前一致。請求用戶支付剩余時間的價格。（本選項僅在更新中可使用） |
| IMMEDIATE\_WITHOUT\_PRORATION           | 定期支付的變更將立即進行，下一個支付日支付新的價格。請求支付周期和之前一致。               |
| DEFERRED                                | 現有套餐到期後立即變更，同時發给用戶新的資費。                              |

#### 升级或降级 <a href="#id-04.-sdk" id="id-04.-sdk"></a>

定期支付可以使用與請求購買同一个API，向用户提供升级或降级。但是，為了適用定期支付的升级與降级，必須具備現有定期支付購買token和比例分配模式值。

如下例子，需要提供現有定期支付、今後（升级或降级）定期支付及比例分配模式的相關資訊。

{% 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 %}

因為升级或降级也會執行請求購買邏輯，所以在`PurchasesUpdatedListener`接收回覆。另外也會在查詢購買明细中得到回應。\
用比例分配模式購買也與一般購買相同，需要使用`PurchaseClient.acknowledgeAsync()`確認購買。

### 購買處理  <a href="#id-04.-sdk" id="id-04.-sdk"></a>

購買完成，需要在應用軟件進行購買確認處理。大部分情况下，應用軟件通過`PurchasesUpdatedListener`接收購買通知。或者如查詢購買明细說明那樣，也有時應用軟件調用`PurchaseClient.queryPurchasesAsync()`函數處理。

可以使用如下方法中的一個確認購買。

* 消耗性產品使用`PurchaseClient.consumeAsync()`
* 非消耗性產品使用`PurchaseClient.acknowledgeAsync()`

#### 使用管理型商品(consume) <a href="#id-04.-sdk-consume" id="id-04.-sdk-consume"></a>

管理型商品在使用之前不能再次購買。

為了使用商品，需要調用`consumeAsync()`。另外，要想接收使用操作结果，需要展現`ConsumeListener` 接口。

{% hint style="info" %}
不使用管理型商品時，可以作为永久性形態的商品類型使用，購買後立即消費時，也可以作为消費性形態的商品使用。\
另外，如果特定期間以後消費，可以作为固定期限型態的商品使用。
{% endhint %}

{% hint style="danger" %}
如果3天内不確認購買(acknowledge)或不使用(consume)，判断為没有向用戶提供商品，自動退款给用戶。
{% 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" %}
使用請求有時會失敗，因此必須檢查安全後端伺服器，確認各購買token是否使用。只有這樣應用軟件才不會對同一個購買進行多次授權。或者，在授權之前，可以等到成功消費回覆为止 。
{% endhint %}

#### 確認購買(acknowledge) <a href="#id-04.-sdk-acknowledge" id="id-04.-sdk-acknowledge"></a>

可以使用`PurchaseClient.acknowledgeAsync()` 函數確認處理非使用型商品的購買。管理型商品、包月型商品、訂閱型商品均可使用。

使用`PurchaseData.isAcknowledged()` 函数判断是否已確認購買。另外，如果想要接收對購買確認的操作结果，需要展現`AcknowledgeListener`接口。

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

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

{% endtab %}

{% tab title="Java" %}

```java
// Purchase retrieved from PurchaseClient#queryPurchasesAsync or your PurchasesUpdatedListener.
private void handlePurchase(purchase: PurchaseData) {
    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" %}
以`AcknowledgeListener.onAcknowledgeResponse()` 函數傳送的`PurchaseData`是請求時的數據，因此`acknowledgeState`值不變。需要通过查询購買明细更換成變更的數據。
{% endhint %}

### 查詢購買明细 <a href="#id-04.-sdk" id="id-04.-sdk"></a>

僅使用`PurchasesUpdatedListener`處理購買，不足以保證應用軟件已經處理所有購買。應用軟件可能錯過購買跟踪或無法識別購買等幾種情况如下。

在如下幾種情况，可能發生應用軟件沒接收購買回覆或無法識別購買。

* 網络問題：用戶成功購買，ONE store上也接收確認，

但是設備在通過*PurchasesUpdatedListener*接收購買通知前，發生網絡連接斷開。

* 多台設備：用戶在一台設備上購買商品後，切換到其他設備。

為了應對這種情况，需要在應用軟件的`onCreate()`或 `onResume()`中調用`PurchaseClient.queryPurchasesAsync()`方法來確認購買是否已經成功處理。

回朔處理與`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 %}

### 更改包月型商品狀態 (Deprecated) <a href="#id-04.-sdk-deprecated" id="id-04.-sdk-deprecated"></a>

包月型商品是最初購買後下个月同一天更新的商品。包月型商品的狀態可以通過`PurchaseData.getRecurringState()`來確認。

使用`PurchaseClient.manageRecurringProductAsync()`更改包月型商品的狀態。在`RecurringProductParams` 對象輸入購買數據和要變更的`PurchaseClient.RecurringAction`值。

從SDK V21 (API V7)開始，無法開發新的包月型商品。請使用支付周期為一個月的訂閱型商品。

{% 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" %}
以`RecurringProductListener.onRecurringResponse()` 函數傳送的`PurchaseData`是請求時的數據，所以`recurringState`值不變。需要通過查詢購買明细更換更改的數據。
{% endhint %}

### 打开定期支付管理界面 <a href="#id-04.-sdk" id="id-04.-sdk"></a>

可以顯示管理定期支付中商品的界面。

作為參數輸入包含`PurchaseData`的`SubscriptionsParams`，確認購買數據，可以運行相應定期支付商品的管理界面。\
但是`SubscriptionParams`加入*null*時，運行用戶的定期支付列表界面。

如下是顯示定期支付管理界面的方法的示例。

{% 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 %}

### 獲取市場分類代碼（code） <a href="#id-04.-sdk" id="id-04.-sdk"></a>

從SDK v19以上版本開始，為了使用Server API需要市場分類代碼。\
可以通過`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 %}

### 安裝ONE store服務 <a href="#id-04.-sdk" id="id-04.-sdk"></a>

ONE store服務的版本低或者沒有時，無法使用app内支付。 通過`PurchaseClient.startConnection()`連接時，可以在`IapResult.getResponseCode()`確認。如果發生`RESULT_NEED_UPDATE`，需要調用`launchUpdateOrInstallFlow()`方法。

{% 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 %}


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://onestore-dev.gitbook.io/dev/cht/tools/billing/v21/sdk.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
