# 接入自动续期订阅

#### 约束与限制

自动续期订阅能力支持Phone、Tablet、PC/2in1设备，并且从5.1.1(19)版本开始，新增支持TV设备，从26.0.0版本开始，新增支持Car设备。

#### 业务流程

如下业务流程对于单机应用同样适用。在单机应用中，应用服务器和应用客户端的交互放在应用客户端完成，应用服务器和IAP服务器交互的部分可不处理。

**展示商品**

1.  应用客户端向IAP Kit发起queryEnvironmentStatus请求，检查当前用户登录的华为账号所在的服务地是否在IAP Kit支持结算的国家/地区中。
    
    如果接口返回错误码“1001860054：用户账号所在服务地不在IAP Kit支持结算的国家/地区中”，应用需隐藏相关IAP功能入口。
    
2.  应用客户端向IAP Kit发起queryProducts请求来获取在AppGallery Connect上配置的商品信息。
    
3.  应用客户端根据返回的商品信息展示可供购买的商品列表，包含商品名称、价格等信息。
    

**检查权益发放状态**

1.  应用客户端向IAP Kit发起queryPurchases请求，获取当前生效中的订阅列表。IAP Kit返回PurchaseData列表。PurchaseData为JWS格式的字符串，承载了相关的订阅信息。
    
2.  应用客户端展示商品的订阅状态，需要屏蔽处于自动续期状态的商品的购买入口。同时处理商品的权益发放。
    
3.  若商品未确认发货，需要在权益发放后，向IAP Kit发送finishPurchase请求，以此通知IAP服务器更新商品的发货状态，完成购买流程。应用成功执行finishPurchase之后，IAP服务器会将相应商品标记为已发货状态。
    
    此步骤也可放到应用服务器处理。应用服务器可通过请求服务端订阅确认发货接口来确认发货，完成购买流程。
    

具体请参见对生效中的订阅发放权益。

**购买及结果确认**

1.  用户发起购买后，应用客户端向IAP Kit发起createPurchase购买请求或通过IAP嵌入式收银台组件发起购买请求（只支持TV），请求中携带商品ID、商品类型等信息。IAP Kit创建订单并展示收银台。
    
2.  购买结果确认。如购买成功，可通过应用客户端或应用服务器接收购买结果，建议通过应用服务器接收购买结果。
    
    **方式一：通过客户端接收购买结果**
    
    1.  用户购买成功时，IAP Kit返回包含订阅状态信息的PurchaseData数据。
    2.  应用客户端向应用服务器上报PurchaseData数据。
    3.  应用服务器对PurchaseData.jwsSubscriptionStatus进行解码验签，成功后可得到SubGroupStatusPayload的JSON字符串。
    
    **（建议）方式二：通过服务器接收购买结果**
    
    1.  为了提高安全性，开发者可以接入服务端关键事件通知，在用户购买成功时，IAP服务器将发送订单关键事件通知。
        
    2.  应用服务器可从NotificationPayload.NotificationMetaData中解析出purchaseToken和purchaseOrderId信息，并通过服务端订阅状态查询接口向IAP服务器查询最新的订阅状态信息，进一步确认订阅信息的准确性。
        
    3.  IAP服务器返回订阅组相关订阅状态数据jwsSubGroupStatus。
        
    4.  应用服务器对jwsSubGroupStatus进行解码验签，成功后可得到SubGroupStatusPayload的JSON字符串。
        

如果购买失败，请参见确保权益发放处理，及时发放权益。

**发放权益**

1.  确认购买成功后，需要处理权益发放。检查SubGroupStatusPayload.lastSubscriptionStatus.lastPurchaseOrder是否已发放权益，未发放则需发放相关权益，并记录对应的订单信息（PurchaseOrderPayload），用于后续检查权益发放状态。
    
    建议单机应用将用户权益和订阅状态关联。如果订阅处于生效状态，始终为用户发放权益。
    
2.  应用客户端向应用服务器查询订单的发货状态。
    
3.  应用服务器返回对应的发货状态以及订单信息（PurchaseOrderPayload）。
    
4.  发货成功后应用客户端向IAP Kit发送finishPurchase请求，以此通知IAP服务器更新商品的发货状态，完成购买流程。应用成功执行finishPurchase之后，IAP服务器会将相应商品标记为已发货状态，后续该商品即可正常续期。
    
    此步骤也可放到应用服务器处理。应用服务器可通过请求服务端订阅确认发货接口来确认发货，完成购买流程。
    
    对于自动续期订阅商品，如果不执行此步骤，会导致后续自动续期无法扣费，以及同一个订阅组不同自动续期订阅商品无法切换等问题。
    

#### 开发步骤

#### 展示商品

1.  检查应用引入IAP Kit的可用性。
    
    在使用应用内支付之前，应用客户端需要向IAP Kit发送queryEnvironmentStatus请求，以此判断用户当前登录的华为账号所在的服务地是否在IAP Kit支持结算的国家/地区中。
    
    当前IAP Kit支持结算的国家/地区仅有中国境内（香港特别行政区、澳门特别行政区、中国台湾除外）。
    

```
import { iap } from '@kit.IAPKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { common } from '@kit.AbilityKit';
import Logger from '../common/Logger';
// ...
    const queryEnvCode = await this.queryEnv();
    if (queryEnvCode !== 0) {
      let queryEnvFailedText = 'This app does not support iap';
      if (queryEnvCode === iap.IAPErrorCode.ACCOUNT_NOT_LOGGED_IN) {
        queryEnvFailedText = 'Go to Settings and log in to your Huawei ID and try again.';
      }
      this.showFailedPage(queryEnvFailedText);
      return;
    }
    // ...
  async queryEnv(): Promise<number> {
    return new Promise<number>((resolve) => {
      iap.queryEnvironmentStatus(this.context).then(() => {
        Logger.info(TAG, 'Succeeded in querying environment status.');
        resolve(0);
      }).catch((err: BusinessError) => {
        Logger.error(TAG, `Failed to query environment status. Code is ${err.code}, message is ${err.message}`);
        resolve(err.code);
      });
    });
  }
```

1.  展示商品列表。
    
    应用客户端通过queryProducts来获取在AppGallery Connect上配置的商品信息。发起请求时，需在请求参数QueryProductsParameter中携带相关的商品ID，并指定其productType为iap.ProductType.AUTORENEWABLE。
    
    当接口请求成功时，IAP Kit将返回商品信息Product的列表。 应用可以使用Product包含的商品价格、名称和描述等信息，向用户展示可供购买的商品列表。
    
    queryProducts每次只能查询一种商品类型的商品，每次最多查询200个商品，否则请求将报错。
    

```
import { iap } from '@kit.IAPKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { common } from '@kit.AbilityKit';
import Logger from '../common/Logger';
// ...
  async queryProducts(): Promise<number> {
    const productIds: string[] = ['Sub001', 'KH003', 'FH301'];
    return new Promise<number>((resolve) => {
      iap.queryProducts(this.getUIContext().getHostContext() as common.UIAbilityContext, productIds).then((result) => {
        Logger.info(TAG, 'Succeeded in querying products.');
        // 展示产品详情
        this.productInfoArray = result;
        resolve(0);
      }).catch((err: BusinessError) => {
        // 查询商品报错
        Logger.error(TAG, `Failed to query products. Code is ${err.code}, message is ${err.message}`);
        this.showFailedPage();
        resolve(err.code);
      });
    });
  }
```

#### 展示订阅状态、发放权益

1.  应用获取用户当前生效中的订阅列表。
    
2.  应用客户端展示对应商品的订阅状态。此处需要屏蔽处于自动续期状态的商品的购买入口。
    
3.  处理生效中的订阅的权益发放。
    

具体可参见对生效中的订阅发放权益。

#### 发起购买

用户发起购买时，应用可通过向IAP Kit发送createPurchase请求来拉起IAP Kit收银台或通过IAP嵌入式收银台组件发起购买请求（只支持TV）。发起请求时，应用需在请求参数PurchaseParameter中携带此前已在华为AppGallery Connect网站上配置并生效的自动续期订阅的商品ID，并指定其productType为iap.ProductType.AUTORENEWABLE。

开发过程中易出现频繁调用接口的现象，建议控制接口调用频度，具体可参见1001860004 接口访问过频。

```
import { iap } from '@kit.IAPKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { common } from '@kit.AbilityKit';
import Logger from '../common/Logger';
// ...
  subscribe(id: string, type: iap.ProductType) {
    try {
      const parameter: iap.PurchaseParameter = {
        productId: id,
        productType: type,
        developerPayload: 'test developer payload string.',
      };
      iap.createPurchase(this.getUIContext().getHostContext() as common.UIAbilityContext, parameter,
        (err: BusinessError, data: iap.CreatePurchaseResult) => {
          if (err) {
            // 请求失败
            const msg: string = `Failed to create purchase. Code is ${err.code}, message is ${err.message}`;
            Logger.error(TAG, msg);
            // 购买失败
            // ...
            return;
          }
          // 请求成功
          const msg: string = 'Succeeded in creating purchase.';
          Logger.info(TAG, msg);
          // 购买成功，处理购买结果
          this.dealPurchaseData(data.purchaseData);
        });
    } catch (err) {
      const e: BusinessError = err as BusinessError;
      const msg: string = `Failed to create purchase. Code is ${e.code}, message is ${e.message}`;
      Logger.error(TAG, msg);
    }
  }
```

#### 购买结果处理

**【结果1：购买成功】**

1.  为了提高安全性，建议应用服务器接入服务端关键事件通知以接收购买成功结果并通过应用服务器来处理解码验签、完成购买等操作。
    
2.  请务必确保发货成功后再执行完成购买步骤，本步骤可通过请求服务端订阅确认发货接口来确认发货，完成购买流程。
    

以下内容为**通过客户端接收购买结果**及处理的步骤说明。

1.  当用户购买成功时，应用将接收到一个CreatePurchaseResult对象，其purchaseData字段包括了此次购买的结果信息。
    
2.  对purchaseData.jwsSubscriptionStatus进行解码验签，验证成功可得到SubGroupStatusPayload的JSON字符串。建议应用客户端将purchaseData发送至应用服务器，在应用服务器执行此操作。
    
3.  验签成功后，检查SubGroupStatusPayload.lastSubscriptionStatus.status是否为1（生效中），是则发放相关权益。
    
    建议先检查此笔订单权益的发放状态，未发放则发放权益，成功后记录SubGroupStatusPayload.lastSubscriptionStatus.lastPurchaseOrder等信息，用于后续检查权益发放状态。
    
4.  完成购买。
    
    发放权益后，应用需发送finishPurchase请求确认发货，以此通知IAP服务器更新商品的发货状态，完成购买流程。发送finishPurchase请求时，需在请求参数FinishPurchaseParameter中携带PurchaseOrderPayload中的productType、purchaseToken、purchaseOrderId，其中PurchaseOrderPayload为SubGroupStatusPayload.lastSubscriptionStatus.lastPurchaseOrder。请求成功后，IAP服务器会将相应商品标记为已发货。
    
    JWSUtil为自定义类，可参见示例代码。
    

```
import { iap } from '@kit.IAPKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { common } from '@kit.AbilityKit';
import Logger from '../common/Logger';
import { JWSUtil } from '../common/JWSUtil';
import {
  FinishStatus,
  PurchaseData,
  PurchaseOrderPayload,
  SubGroupStatusPayload,
  SubStatus,
} from '../common/IapDataModel';
// ...
  dealPurchaseData(purchaseData: string) {
    try {
      // 建议您将 purchaseData 发送到应用服务器进行签名验证。
      const jwsSubscriptionStatus = (JSON.parse(purchaseData) as PurchaseData).jwsSubscriptionStatus;
      if (!jwsSubscriptionStatus) {
        Logger.error(TAG, 'dealPurchaseData, jwsSubscriptionStatus invalid');
        return;
      }
      // 解码 jwsPurchaseOrder 并执行签名验证。
      const subscriptionStatus = JWSUtil.decodeJwsObj(jwsSubscriptionStatus);
      if (!subscriptionStatus) {
        Logger.error(TAG, 'dealPurchaseData, subscriptionStatus invalid');
        return;
      }
      // 需自定义SubGroupStatusPayload类，包含的信息请参见SubGroupStatusPayload
      const subGroupStatusPayload = JSON.parse(subscriptionStatus) as SubGroupStatusPayload;
      const lastSubscriptionStatus = subGroupStatusPayload.lastSubscriptionStatus;
      if (!lastSubscriptionStatus) {
        Logger.error(TAG, 'dealPurchaseData, lastSubscriptionStatus is invalid');
        return;
      }
      if (lastSubscriptionStatus.status === SubStatus.ACTIVE) {
        // 订阅已生效，您需要发货。
        const productId = lastSubscriptionStatus.renewalInfo?.productId;
        if (productId) {
          this.setProductInfoStatus(subGroupStatusPayload.subGroupId, productId, lastSubscriptionStatus.status);
        }
        // 在执行以下步骤之前，请确保发货成功。
      }
      const purchaseOrderPayload = lastSubscriptionStatus.lastPurchaseOrder;
      if (purchaseOrderPayload && purchaseOrderPayload.finishStatus !== FinishStatus.FINISHED) {
        // 向IAP Kit发送finishPurchase请求，以确认商品已发货并完成购买。
        this.finishPurchase(purchaseOrderPayload);
      }
    } catch (e) {
      Logger.error(TAG, 'dealPurchaseData json error');
    }
  }

  finishPurchase(purchaseOrder: PurchaseOrderPayload) {
    if (!purchaseOrder.productType) {
      Logger.error(TAG, 'finishPurchase but productType is empty');
      return;
    }
    const finishPurchaseParam: iap.FinishPurchaseParameter = {
      productType: Number(purchaseOrder.productType),
      purchaseToken: purchaseOrder.purchaseToken,
      purchaseOrderId: purchaseOrder.purchaseOrderId,
    };
    iap.finishPurchase(this.context, finishPurchaseParam).then(() => {
      Logger.info(TAG, 'Succeeded in finishing purchase.');
    }).catch((err: BusinessError) => {
      Logger.error(TAG, `Failed to finish purchase. Code is ${err.code}, message is ${err.message}`);
    });
  }
```

**【结果2：购买失败】**

当用户购买失败时，需要针对code为iap.IAPErrorCode.PRODUCT\_OWNED和iap.IAPErrorCode.SYSTEM\_ERROR的场景，检查是否需要补发货，确保权益发放，具体请参见确保权益发放。

```
if (err.code === iap.IAPErrorCode.PRODUCT_OWNED || err.code === iap.IAPErrorCode.SYSTEM_ERROR) {
  // 参见确保权益发放检查是否需要补发货，确保权益发放
  // ...
  // ...
}
```