目录

权益发放

对生效中的订阅发放权益

[h2]场景介绍

用户购买自动续期订阅商品后,若订阅处于生效状态,开发者需要及时给用户发放对应权益。

在应用启动时,获取用户当前处于生效状态的订阅列表,处理此部分订阅的权益发放。建议先检查当前订阅对应权益的发放状态,未发放再补充发放权益。在权益发放成功后,向IAP确认发货,完成购买。

建议单机应用将用户权益和订阅状态关联。如果订阅处于生效状态,始终为用户发放权益。

[h2]业务流程

  1. 应用客户端向IAP Kit发起queryPurchases请求,查询用户生效中的订阅列表。

  2. IAP Kit返回PurchaseData列表。PurchaseData为JWS格式的字符串,承载了相关的订阅信息。

  3. 应用客户端向应用服务器上报PurchaseData列表。

  4. 应用服务器需对每个PurchaseData.jwsSubscriptionStatus进行解码验签,验证成功可得到对应的SubGroupStatusPayload的JSON字符串。

  5. 处理权益发放。检查SubGroupStatusPayload.lastSubscriptionStatus.lastPurchaseOrder是否已发放权益,未发放则需发放相关权益,并记录对应的订单信息(PurchaseOrderPayload)。

    建议单机应用将用户权益和订阅状态关联。如果订阅处于生效状态,始终为用户发放权益。

  6. 应用客户端向应用服务器查询订单的发货状态。

  7. 应用服务器返回对应的发货状态以及订单信息(PurchaseOrderPayload)。

  8. 发放权益后应用客户端向IAP Kit发送finishPurchase请求,以此通知IAP服务器更新商品的发货状态,完成购买流程。应用成功执行finishPurchase之后,IAP服务器会将相应商品标记为已发货状态。此步骤也可放到应用服务器处理。应用服务器可通过请求服务端订阅确认发货接口来确认发货,完成购买流程。

    对于自动续期订阅商品,如果不执行此步骤,会导致后续自动续期无法扣费 ,以及同一个订阅组不同自动续期订阅商品无法切换等问题。

[h2]开发步骤

  1. 应用客户端向IAP Kit发起queryPurchases请求,获取生效中的订阅列表。

    在请求参数QueryPurchasesParameter中指定productType为iap.ProductType.AUTORENEWABLE,同时指定queryType为iap.PurchaseQueryType.CURRENT_ENTITLEMENT。当接口请求成功时,IAP Kit将返回一个QueryPurchaseResult对象,该对象包含承载了订阅信息的PurchaseData的列表。

  2. 验证订单信息。对每个purchaseData.jwsSubscriptionStatus进行解码验签,验证成功可得到SubGroupStatusPayload的JSON字符串。建议应用客户端将purchaseData发送至应用服务器,在应用服务器执行此操作。

    为了提高安全性,可从SubGroupStatusPayload.lastSubscriptionStatus.lastPurchaseOrder中解析出purchaseToken和purchaseOrderId信息,并通过服务端订阅状态查询接口向IAP服务器查询最新的订阅状态信息,进一步确认订阅信息的准确性。

  3. 展示订阅状态。

    • 如果SubGroupStatusPayload.lastSubscriptionStatus.status=1,表示订阅处于生效状态。
    • 如果SubGroupStatusPayload.lastSubscriptionStatus.status=1且SubGroupStatusPayload.lastSubscriptionStatus.renewalInfo.autoRenewStatusCode值为1时,表示订阅处于自动续期状态。此状态的商品无法再次购买,需要屏蔽相关的购买入口。
  4. 权益发放。获取SubGroupStatusPayload.lastSubscriptionStatus.lastPurchaseOrder(下文标记为PurchaseOrderPayload),处理权益发放。

    可先检查此笔订单权益的发放状态,未发放则补充发放权益,成功后记录PurchaseOrderPayload等信息,用于后续检查权益发放状态。

    建议单机应用将用户权益和订阅状态关联。如果订阅处于生效状态,始终为用户发放权益。

  5. 在发放权益后,如果PurchaseOrderPayload.finishStatus不为1,应用需调用finishPurchase接口确认发货,完成购买流程。

    发起请求时,需在请求参数FinishPurchaseParameter中携带PurchaseOrderPayload中的productType、purchaseToken、purchaseOrderId。请求成功后,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';
// ...
    await this.queryPurchases(iap.PurchaseQueryType.CURRENT_ENTITLEMENT);
    // ...
  queryPurchases(queryType: iap.PurchaseQueryType): Promise<void> {
    return new Promise<void>((resolve) => {
      const param: iap.QueryPurchasesParameter = {
        productType: iap.ProductType.AUTORENEWABLE,
        queryType: queryType,
      };
      iap.queryPurchases(this.context, param).then((res: iap.QueryPurchaseResult) => {
        Logger.info(TAG, 'Succeeded in querying purchases.');
        const purchaseDataList: string[] = res.purchaseDataList;
        if (purchaseDataList === undefined || purchaseDataList.length <= 0) {
          Logger.info(TAG, 'queryPurchases, purchaseDataList empty');
          resolve();
          return;
        }
        for (let i = 0; i < purchaseDataList.length; i++) {
          this.dealPurchaseData(purchaseDataList[i]);
        }
        resolve();
      }).catch((err: BusinessError) => {
        Logger.error(TAG, `Failed to query purchases. Code is ${err.code}, message is ${err.message}`);
        resolve();
      }).finally(() => {
        this.showNormalPage();
      });
    });
  }

  dealPurchaseData(purchaseData: string) {
    try {
      // 建议您将purchaseData发送到应用服务器进行签名验证。
      const jwsSubscriptionStatus = (JSON.parse(purchaseData) as PurchaseData).jwsSubscriptionStatus;
      if (!jwsSubscriptionStatus) {
        Logger.error(TAG, 'dealPurchaseData, jwsSubscriptionStatus invalid');
        return;
      }
      // 解码jwsSubscriptionStatus并执行签名验证。
      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}`);
    });
  }

确保权益发放

用户购买自动续期订阅成功或者自动续期成功后,开发者需要及时给用户发放相关权益。但实际应用场景中,若出现异常(网络错误等)将导致应用无法知道用户实际是否支付成功,从而无法及时发放权益,即出现掉单情况。

为了确保权益发放,需要在createPurchase请求返回iap.IAPErrorCode.PRODUCT_OWNED或iap.IAPErrorCode.SYSTEM_ERROR时检查用户是否存在已购但未确认发货的商品,如果存在则发放相关权益,然后向IAP Kit确认发货,完成购买。

[h2]业务流程

  1. 应用客户端向IAP Kit发起queryPurchases请求,查询用户已购买但未确认发货的订阅列表。

  2. IAP Kit返回PurchaseData列表。PurchaseData为JWS格式的字符串,承载了相关的订阅信息。

  3. 应用客户端向应用服务器上报PurchaseData列表。

  4. 应用服务器需对每个PurchaseData.jwsSubscriptionStatus进行解码验签,验证成功可得到对应的SubGroupStatusPayload的JSON字符串。

  5. 处理权益发放。检查SubGroupStatusPayload.lastSubscriptionStatus.lastPurchaseOrder是否已发放权益,未发放则需发放相关权益,并记录对应的订单信息(PurchaseOrderPayload)。

    建议单机应用将用户权益和订阅状态关联。如果订阅处于生效状态,始终为用户发放权益。

  6. 应用客户端向应用服务器查询订单的发货状态。

  7. 应用服务器返回对应的发货状态以及订单信息(PurchaseOrderPayload)。

  8. 发放权益后应用客户端向IAP Kit发送finishPurchase请求,以此通知IAP服务器更新商品的发货状态,完成购买流程。应用成功执行finishPurchase之后,IAP服务器会将相应商品标记为已发货状态。此步骤也可放到应用服务器处理。应用服务器可通过请求服务端订阅确认发货接口来确认发货,完成购买流程。

    对于自动续期订阅商品,如果不执行此步骤,会导致后续自动续期无法扣费 ,以及同一个订阅组不同自动续期订阅商品无法切换等问题。

[h2]开发步骤

  1. 应用客户端向IAP Kit发起queryPurchases请求,获取用户已购但未确认发货的订阅列表。

    在请求参数QueryPurchasesParameter中指定productType为iap.ProductType.AUTORENEWABLE,同时指定queryType为iap.PurchaseQueryType.UNFINISHED。当接口请求成功时,IAP Kit将返回一个QueryPurchaseResult对象,该对象包含承载了订阅信息的PurchaseData的列表。

  2. 验证订单信息。对每个purchaseData.jwsSubscriptionStatus进行解码验签,验证成功可得到SubGroupStatusPayload的JSON字符串。建议应用客户端将purchaseData发送至应用服务器,在应用服务器执行此操作。

    为了提高安全性,可从SubGroupStatusPayload.lastSubscriptionStatus.lastPurchaseOrder中解析出purchaseToken和purchaseOrderId信息,并通过服务端订阅状态查询接口向IAP服务器查询最新的订阅状态信息,进一步确认订阅信息的准确性。

  3. 处理权益发放。

    如果SubGroupStatusPayload.lastSubscriptionStatus.status=1,表示订阅处于生效状态。需要对生效状态的订阅处理权益发放。建议先检查此笔订单权益的发放状态,未发放则补充发放权益,成功后记录PurchaseOrderPayload等信息,用于后续检查权益发放状态。

    建议单机应用将用户权益和订阅状态关联。如果订阅处于生效状态,始终为用户发放权益。

  4. 在发放权益后,如果PurchaseOrderPayload.finishStatus不为1,应用需调用finishPurchase接口确认发货,完成购买流程。

    发起请求时,需在请求参数FinishPurchaseParameter中携带PurchaseOrderPayload中的productType、purchaseToken、purchaseOrderId。请求成功后,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';
// ...
  queryPurchases(queryType: iap.PurchaseQueryType): Promise<void> {
    return new Promise<void>((resolve) => {
      const param: iap.QueryPurchasesParameter = {
        productType: iap.ProductType.AUTORENEWABLE,
        queryType: queryType,
      };
      iap.queryPurchases(this.context, param).then((res: iap.QueryPurchaseResult) => {
        Logger.info(TAG, 'Succeeded in querying purchases.');
        const purchaseDataList: string[] = res.purchaseDataList;
        if (purchaseDataList === undefined || purchaseDataList.length <= 0) {
          Logger.info(TAG, 'queryPurchases, purchaseDataList empty');
          resolve();
          return;
        }
        for (let i = 0; i < purchaseDataList.length; i++) {
          this.dealPurchaseData(purchaseDataList[i]);
        }
        resolve();
      }).catch((err: BusinessError) => {
        Logger.error(TAG, `Failed to query purchases. Code is ${err.code}, message is ${err.message}`);
        resolve();
      }).finally(() => {
        this.showNormalPage();
      });
    });
  }

  dealPurchaseData(purchaseData: string) {
    try {
      // 建议您将purchaseData发送到应用服务器进行签名验证。
      const jwsSubscriptionStatus = (JSON.parse(purchaseData) as PurchaseData).jwsSubscriptionStatus;
      if (!jwsSubscriptionStatus) {
        Logger.error(TAG, 'dealPurchaseData, jwsSubscriptionStatus invalid');
        return;
      }
      // 解码jwsSubscriptionStatus并执行签名验证。
      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}`);
    });
  }
  // ...
              this.queryPurchases(iap.PurchaseQueryType.UNFINISHED);