# 在Unity中使用One store In-App支付

## 概要

ONE store支付插件在Unity环境拓展asset，提供在游戏中one store支付程序库的最新功能。本指南对设置project使用插件的方法，以及展现ONE store支付程序库功能的方法进行说明。

### 研发版本

<table data-view="cards"><thead><tr><th></th><th></th></tr></thead><tbody><tr><td><strong>Unity</strong></td><td>2022.3.62f3</td></tr><tr><td><strong>Java SDK (Java 11)</strong></td><td><p>Purchase: v21.04.00</p><p>App License Checker: v2.2.1</p></td></tr></tbody></table>

## ONE store支付插件设置

### 插件下载及导入（import）

1. 可以在GitHub的 Release 页面下载Unity用ONE store app内支付插件的最新版本。
2. 在Unity菜单栏点击**Assets > Import Package > Custom Package**<br>

   <figure><img src="https://2757207078-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FG1QmZshNU0saLqpjlDho%2Fuploads%2FEVG1NORCSJhfYHkbLcni%2F1.png?alt=media&#x26;token=d46885ba-b39c-472a-ab05-9df59b39fb93" alt=""><figcaption></figcaption></figure>
3. 找到下载位置选择.unitypackage文件。
4. **Import Unity Package**对话框内所有Assets都选上，点击**Import**。

导入Package生成新文件夹。这个文件夹里包括ONE store支付程序库。

* Assets/OneStoreCorpPlugins
  * /Common
  * /Authentication
  * /Purchase
  * /AppLicenseChecker

[EDM4U(External Dependency Manager for Unity)](https://github.com/googlesamples/unity-jar-resolver)必须一起发行。\
如果已经在使用，在Import Package阶段解除检测ExternalDependencyManager适用即可。

### 包含外部从属性

要在Project包含仓库及从属性，需要遵循如下阶段。

**`Project Settings > Player > Publishing Settings > Build`**

检测以下两方面

* Custom Main Manifest
* Custom Main Gradle Template<br>

  <figure><img src="https://2757207078-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FG1QmZshNU0saLqpjlDho%2Fuploads%2FpEzpz9OTWRH0G2S3bqr8%2F2.png?alt=media&#x26;token=69786c3b-9c95-43fa-974f-3406559c8539" alt=""><figcaption></figcaption></figure>

选择 **`Assets > External Dependency Manager > Android Resolver > Force Resolve`**

可以在`mainTemplete.gradle`文件确认适用如下内容。<br>

<figure><img src="https://2757207078-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FG1QmZshNU0saLqpjlDho%2Fuploads%2FBmzCRaDL9y6qxQGh7tk9%2F3.png?alt=media&#x26;token=c608bcf9-f7d2-4fd9-ada7-b25cc7f3b721" alt=""><figcaption></figcaption></figure>

{% hint style="info" %}

* **In-app Purchase从**属性列出如下。\
  Assets/OneStoreCorpPlugins/Purchase/Editor/PurchaseDependencies.xml
* **App License Checker** 从属性列出如下。\
  Assets/OneStoreCorpPlugins/AppLicenseChecker/Editor/AppLicenseCheckerDependencies.xml
  {% endhint %}

### \<queries> 设置

需要在`AndroidManifest.xml`文件设置。详细内容请参考公告事项。

{% hint style="danger" %}
如果不设&#x7F6E;**\<queries>**, 就无法在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>
```

### 发行全球store的测试选项设置

#### 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" %}
&#x20;发行组建版本中，本选项必须删除。
{% endhint %}

## 游戏中适用ONE store app内支付程序库

### 设置 Log level

在研发阶段设置Log level，可以更仔细显示SDK的数据流向。以[android.util.Log](https://developer.android.com/reference/android/util/Log#summary)内定义的值为基础运作。

```csharp
using OneStore.Common;
/// <summary>
/// Warning! This code must be deleted during release build as it may be vulnerable to security.
/// Use <seealso cref="OneStoreLogger.SetLogLevel(int)"/> only for development.
/// </summary>
OneStoreLogger.SetLogLevel(2);
```

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

{% hint style="danger" %}
发行组建版本网络安全比较弱，本选项需要删除。
{% endhint %}

### ONE store app内支付初始化

<figure><img src="https://2757207078-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FG1QmZshNU0saLqpjlDho%2Fuploads%2FjBHeW2qAxQ6N9izzL2f2%2F4.png?alt=media&#x26;token=25fdecf3-6b6e-4c4a-a48e-8ba7250a592f" alt=""><figcaption></figcaption></figure>

为了请求app内支付，需要使用ONE store研发者中心提供的许可证密钥对`PurchaseClientImpl`对象进行初始化。  调用`Initialize()`函数完成基本配置。这是为了连接ONE store服务的先行操作。

```csharp
using OneStore.Purchasing;

class YourCallback: IPurchaseCallback
{
    // IPurchaseCallback implementations.
}

var purchaseClient = new PurchaseClientImpl("your license key");
purchaseClient.Initialize(new YourCallback());
```

### 查询商品信息

`PurchaseClientImpl`对象初始化结束后，如果通过`QueryProductDetails()`请求商品信息，以`IPurchaseCallback.OnProductDetailsSucceeded()`接收回复。

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

```csharp
using OneStore.Purchasing;
  
List items = ...
purchaseClient.QueryProductDetails(items.AsReadOnly(), [ProductType.ALL | ProductType.INAPP | ProductType.SUBS]);

// IPurchaseCallback implementations
public void OnProductDetailsSucceeded(List productDetails)
{
    ...
}

public void OnProductDetailsFailed(IapResult iapResult)
{
    ...
}
```

{% hint style="warning" %}
应用程序内商品ID列表要在研发者的网络安全后端服务器上管理。
{% endhint %}

### 请求购买

为了在app请求购买，要在基本线程调用`Purchase()`函数。

以调用`QueryProductDetails()`API获取的`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 %}

```csharp
  using OneStore.Purchasing;
  
  ProductDetail productDetail = ...
  ProductType productType = ProductType.Get(productDetail.type);

  var purchaseFlowParams = new PurchaseFlowParams.Builder()
          .SetProductId(productId)                // mandatory
          .SetProductType(productType)            // mandatory
          .SetDeveloperPayload(developerPayload)  // optional
          .SetQuantity(quantity)                  // optional
          // .SetProductName(null)                // optional: Change the name of the product to appear on the purchase screen.
          
          // It should be used only in advance consultation with the person in charge of the One Store business, and is not normally used.
          // .SetGameUserId(null)                 // optional: User ID to use for promotion.
          // .SetPromotionApplicable(false)       // optional: Whether to participate in the promotion.
          .Build(); 

  purchaseClient.Purchase(purchaseFlowParams);
```

如果成功调用`Purchase()`，显示如下界面。【图片1】显示管理型商品支付购买界面。

<figure><img src="https://2757207078-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FG1QmZshNU0saLqpjlDho%2Fuploads%2Fr2eIyBFp2sDxUMEwvOLU%2F5.png?alt=media&#x26;token=6232f1cf-0869-42d6-b0d3-b1420470444e" alt=""><figcaption><p>【图片1】</p></figcaption></figure>

如果购买成功，向`IPurchaseCallback.OnPurchaseSucceeded()`函数传送结果。

失败时调用`IPurchaseCallback.OnPurchaseFailed()` 函数。

```csharp
  using OneStore.Purchasing;
  
  // IPurchaseCallback implementations
  public void OnPurchaseSucceeded(List purchases)
  {
      handlePurchase(purchases);
  }

  public void OnPurchaseFailed(IapResult iapResult)
  {
      ...
  }
```

<figure><img src="https://2757207078-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FG1QmZshNU0saLqpjlDho%2Fuploads%2F9v8bT6vZPyyzvVv4wz2W%2F6.png?alt=media&#x26;token=c26d16f8-153b-4857-b479-910d15f8ed51" alt=""><figcaption></figcaption></figure>

如果购买成功， 购买数据还会生成用户及商品ID的固有识别符号购买token。购买token虽然可以在应用软件内储存，但是最好将token传送到验证购买、避免诈骗的后端服务器。

管理型商品和定期支付商品的购买token在每次支付的时候都会发行购买token。（包月型商品自动支付更新期间，购买token将保持不变。）

另外，用户会通过电子邮件接收包含收据号码的交易收据。管理型商品每次购买都会接收电子邮件，包月型商品和定期支付商品在第一次购买和之后有更新的时候会接收电子邮件。

### 定期支付 <a href="#id-12.unity-sdkv21" id="id-12.unity-sdkv21"></a>

定期支付在取消之前一直自动更新。定期支付可能有以下状态。

* 激活：用户使用内容过程中没有问题的良好状态，可以接触定期支付。
* 预约暂停：用户使用定期支付期间想要暂停时可以选择。
  * 周定期支付：以1-3周为单位进行暂停。
  * 月定期支付：以1-3个月为单位进行暂停。
  * 年定期支付：不予支持暂停。
* 预约解除：虽然用户正在使用定期支付，但想取消时可以选择。下一个支付日将不会支付。
* 延期，保留：如果用户出现支付问题，则无法在下一个支付日支付。用户不能取消预约，可以立即“解除订阅”。

#### 允许用户升级、降级或变更定期支付 <a href="#id-12.unity-sdkv21" id="id-12.unity-sdkv21"></a>

用户想要升级或降级定期支付时，可以在购买时设置比例分配模式，或者设置变更事项影响到定期支付用户的方式。\
下表是可以使用的比例分配模式(`OneStoreProrationMode`)

| 比例分布模式                                  | 说明                                                   |
| --------------------------------------- | ---------------------------------------------------- |
| IMMEDIATE\_WITH\_TIME\_PRORATION        | 定期支付的变更将立即进行，剩余时间基于差价调整后退款或支付。（这是基本操作）               |
| IMMEDIATE\_AND\_CHARGE\_PRORATED\_PRICE | 定期支付的变更将立即进行，请求支付周期和之前一致。请求用户支付剩余时间的价格。（本选项仅在更新中可使用） |
| IMMEDIATE\_WITHOUT\_PRORATION           | 定期支付的变更将立即进行，下一个支付日支付新的价格。请求支付周期和之前一致。               |
| DEFERRED                                | 现有套餐到期后立即变更，同时发给用户新的资费。                              |

#### 升级或降级 <a href="#id-12.unity-sdkv21" id="id-12.unity-sdkv21"></a>

定期支付可以使用与请求购买同一个API，向用户提供升级或降级。但是，为了适用定期支付的升级与降级，必须具备现有定期支付购买token和比例分配模式值。\
如下例子，需要提供现有定期支付、今后（升级或降级）定期支付以及比例分配模式相关信息。

```csharp
using OneStore.Purchasing;
  
public void UpdateSubscription(ProductDetail productDetail, PurchaseData oldPurchase, OneStoreProrationMode mode, string developerPayload)
{
    ProductType productType = ProductType.Get(productDetail.type);
  
    var purchaseFlowParams = new PurchaseFlowParams.Builder()
             .SetProductId(productId)                        // mandatory
             .SetProductType(productType)                    // mandatory
             .SetOldPurchaseToken(oldPurchase.PurchaseToken) // mandatory
             .SetProrationMode(mode)                         // mandatory
  
             .SetDeveloperPayload(developerPayload)          // optional
             // .SetProductName(null)                        // optional: Change the name of the product to appear on the purchase screen.
             .Build(); 

    purchaseClient.UpdateSubscription(purchaseFlowParams);
}
```

因为升级或降级也会执行请求购买逻辑，所以接收同样回复。\
另外，也会在查询购买明细中有请求时接收回复。用比例分配模式购买也和一般购买一样，需要使用`PurchaseClientImpl.AcknowledgePurchase()`进行购买后处理。

### 购买后处理 <a href="#id-12.unity-sdkv21" id="id-12.unity-sdkv21"></a>

如果用户购买完成，需要在应用软件进行购买处理。 大部分情况下，应用软件通过`OnPurchaseSucceeded()`获得购买通知。或者如查询购买明细说明那样，也有应用软件调用`PurchaseClientImpl.QueryPurchases()`函数处理的情况。

#### 消费购买商品 (consume) <a href="#id-12.unity-sdkv21-consume" id="id-12.unity-sdkv21-consume"></a>

管理型商品在消费之前不能再次购买。

为了消费商品，调用`ConsumePurchase()`。另外，消费操作结果调用为`IPurchaseCallback..OnConsumeSucceeded()`。

{% hint style="info" %}
不消费管理型商品时，可以作为永久性形态的商品使用， 购买后立即消费时，也可以作为消费性形态的商品使用。\
另外，如果特定时间后消费，还可以作为固定期限形态的商品使用 。
{% endhint %}

```csharp
using OneStore.Purchasing;

public void handlePurchase(PurchaseData purchaseData)
{
    purchaseClient.ConsumePurchase(purchaseData);
}

// IPurchaseCallback implementations
public void OnConsumeSucceeded(PurchaseData purchase)
{
    ...
}

public void OnConsumeFailed(IapResult iapResult)
{
    ...
}
```

{% hint style="info" %}
消费请求有时会失败， 因此必须检查安全后端服务器，确认各购买token是否使用。只有这样应用软件才不会对同一个购买进行多次授权。或者，在授权之前， 可以等到成功消费回复为止。
{% endhint %}

{% hint style="warning" %}
3天内不确认购买(`acknowledge`)或不消费(`consume`)，判断为没有为用户提供商品，自动退款给用户。
{% endhint %}

#### 验证购买商品 (acknowledge) <a href="#id-12.unity-sdkv21-acknowledge" id="id-12.unity-sdkv21-acknowledge"></a>

如果验证非消费型商品，使用`PurchaseClientImpl.AcknowledgePurchase()`函数。管理型商品、包月型商品、订阅型商品均可使用。

使用`PurchaseData.Acknowledged()` 函数判断是否已验证。另外，验证操作成功后，将调用`IPurchaseCallback.OnAcknowledgeSucceeded()`函数。

```csharp
using OneStore.Purchasing;

public void handlePurchase(PurchaseData purchaseData)
{
    if (!purchaseData.Acknowledged())
    {
         purchaseClient.AcknowledgePurchase(purchaseData);
    }
}

// IPurchaseCallback implementations
public void OnAcknowledgeSucceeded(PurchaseData purchase)
{
    ...
}

public void OnAcknowledgeFailed(IapResult iapResult)
{
    ...
}
```

### 查询购买明细 <a href="#id-12.unity-sdkv21" id="id-12.unity-sdkv21"></a>

仅仅处理购买，不足以保证应用软件处理了所有购买。应用软件可能无法识别用户购买的全部商品。 应用软件可能错过购买跟踪或无法识别购买等几种情况如下。

* 网络问题：用户成功购买，ONE store上也收到确认，但是设备在接收购买通知之前，发生网络连接断开的情况。
* 多个设备：用户在一台设备上购买商品后，切换到其他设备时，期待显示该商品。

为了处理这种情况，需要在应用软件的`Start()`或`OnApplicationPause()`中调用`PurchaseClientImpl.QueryPurchases()`，按照购买后处理说明来确认所有购买是否已经成功处理。

```csharp
using OneStore.Purchasing;
  
// IPurchaseCallback implementations
public void OnPurchaseSucceeded(List purchases)
{
    handlePurchase(purchases);
}

public void OnPurchaseFailed(IapResult iapResult)
{
    ...
}
```

### 更改包月型商品状态 (Deprecated) <a href="#id-12.unity-sdkv21-deprecated" id="id-12.unity-sdkv21-deprecated"></a>

包月型商品是最初购买后30天更新的商品。包月型商品的状态可以通过`PurchaseData.RecurringState()`确认。

如果要更改包月型商品的状态，请使用`PurchaseClientImpl.ManageRecurringProduct()`。输入购买数据和要更改的`RecurringAction`值 。

```csharp
using OneStore.Purchasing;
  
RecurringAction recurringAction = purchaseData.RecurringState == 0 ? RecurringAction.CANCEL : RecurringAction.REACTIVATE;
purchaseClient.ManageRecurringProduct(purchaseData, recurringAction); 

// IPurchaseCallback implementations
public void OnManageRecurringProduct(IapResult iapResult, PurchaseData purchase, RecurringAction action)
{
     if (iapResult.IsSuccessful())
    {
        ...
    }
    else
    {   
        ...
    }
}
```

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

可以显示管理订阅商品状态的界面。\
作为参数输入`PurchaseData`后，确认购买数据，运行相应定期支付商品的管理界面。但是输入`null`时，运行用户的定期支付列表界面。下面是显示定期支付管理界面的方法的示例。

```csharp
using OneStore.Purchasing;
  
PurchaseData purchaseData = ...;
purchaseClient.LaunchManageSubscription(purchaseData); 
```

### 获取市场分类代码（code） <a href="#id-12.unity-sdkv21" id="id-12.unity-sdkv21"></a>

从SDK v19版本以上开始，为了使用S2S API需要市场分类代码。

`PurchaseClientImpl` 对象初始化时，SDK尝试与支付模块连接。此时连接成功就会自动导入`StoreCode`。

`PurchaseClientImpl.StoreCode` 变数有配额。

```csharp
using OneStore.Purchasing;
  
// Possible after calling the PurchaseClientImpl.RetrieveProducts()
var storeCode = purchaseClient.StoreCode;
```

### 安装ONE store服务 <a href="#id-12.unity-sdkv21" id="id-12.unity-sdkv21"></a>

ONE store服务的版本低或者没有时，无法使用app内支付。首次调用API时，首先尝试与ONE store服务连接。此时，如果发生`RESULT_NEED_UPDATE`，则需要调用`LaunchUpdateOrInstallFlow()` 方法。

```csharp
using OneStore.Purchasing;

purchaseClient.LaunchUpdateOrInstallFlow(); 
```

### 请求登录ONE store  <a href="#id-12.unity-sdkv21" id="id-12.unity-sdkv21"></a>

ONE store app内 SDK只要用户登录ONE store才能启动。内部首先尝试以登录token进行登录。失败或最初登录等没有用户信息时，会显示前台登录画面，引导用户登录。

```csharp
using OneStore.Auth;
  
new OneStoreAuthClientImpl().LaunchSignInFlow({signInResult} => {
    if (signInResult.IsSuccessful())
    {
        ...
    }
});
```
