# 使用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 选择功能 &#x20;
* **v21.02.00** : 使用 `android:value` 可设置韩国、新加坡/台湾及美国。&#x20;
* **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负责购买数据伪造篡改相关的签字确认操作。虽然是选项值，但是建议使用。

可以在研发者中心确认许可证密钥。

<figure><img src="https://2757207078-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FG1QmZshNU0saLqpjlDho%2Fuploads%2F5MRbQQBs9cSYw2ydgizg%2Fimage%20(1).png?alt=media&#x26;token=5c97387b-fe12-4057-9d24-74a5fddbabac" 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="https://2757207078-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FG1QmZshNU0saLqpjlDho%2Fuploads%2FqoirSrjQOEa9Nq1tUA4j%2F2.png?alt=media&#x26;token=0280e936-cc14-4eed-870a-5f90b96551b6" alt=""><figcaption></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="https://2757207078-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FG1QmZshNU0saLqpjlDho%2Fuploads%2Fj1YufCUO8lj1VBXYalhG%2F3.png?alt=media&#x26;token=493e4066-50e7-4403-9bf9-c66a8fb47c70" 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="https://2757207078-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FG1QmZshNU0saLqpjlDho%2Fuploads%2FTjjpNSIRhM8DnOnC2msD%2F4.png?alt=media&#x26;token=9438762c-53fa-41ba-ad4c-147fa79077c8" 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(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" %}
以`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/chi/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.
