鸿蒙next内购支付接入教程横空出世

前言导读

最近因为公司的业务需要接入鸿蒙内购支付IPA ,刚好周末有时间所以就整理一下内容分享一下,希望帮助各位同学工作和学习

介绍

IAP Kit(应用内支付服务)为开发者提供便捷的应用内支付体验和简便的接入流程,让开发者聚焦应用本身的业务能力,助力开发者商业变现。开发者应用可通过使用IAP Kit提供的系统级支付API快速启动IAP收银台,即可实现应用内支付。 通过IAP Kit,用户可以在应用内购买各种类型的数字商品(虚拟商品),包括消耗型商品、非消耗型商品、自动续期订阅商品和非续期订阅商品。

  • 消耗型商品:使用一次后即消耗掉,随使用减少,需要再次购买的商品。例:游戏货币,游戏道具等。
  • 非消耗型商品:一次性购买,永久拥有,无需消耗。例:游戏中额外的游戏关卡、应用中无时限的高级会员等。
  • 自动续期订阅商品:用户购买后在一段时间内允许访问增值功能或内容,周期结束后自动续期购买下一期的服务。例:应用中有时限的自动续期高级会员,如视频连续包月会员。
  • 非续期订阅商品:用户购买后在一段时间内允许访问增值功能或内容,周期结束后禁止访问,除非再次购买自动续期订阅或非续期订阅商品。例:应用中有时限的高级会员,如:视频一个月会员。

官方文档地址

效果图

参数准备

在module.json5里面添加 client_id 配置, client_id 请修改成自己应用的id

json 复制代码
"metadata": [
  // 配置如下信息
  {
    "name": "client_id",
    "value": "xxxxxxxxxxx"
    // 华为Client ID
  },
​
],

具体接入

支付初始化
  • 检查自己的应用是否支持内购支付
typescript 复制代码
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);
    })
  });
}

连接华为查询商品id

productIds 请传入贵方公司应用申请商品id

javascript 复制代码
  async queryProducts() {
    const queryProductParam: iap.QueryProductsParameter = {
      productType: iap.ProductType.CONSUMABLE,
      productIds: ['xxxxx']
    };
    iap.queryProducts(this.context, queryProductParam).then((result) => {
      Logger.info(TAG, 'Succeeded in querying products.');
      // show product details
      this.productInfoArray = result;
      this.productInfoArray.forEach((item:iap.Product)=>{
        this.buy(item.id,item.type)
      })
​
    }).catch((err: BusinessError) => {
      // queryProducts error
      Logger.error(TAG, `Failed to query products. Code is ${err.code}, message is ${err.message}`);
​
    });
  }

拉起鸿蒙内购支付

typescript 复制代码
buy(id: string, type: iap.ProductType) {
  try {
    const createPurchaseParam: iap.PurchaseParameter = {
      productId:id,
      productType: type,
    }
    iap.createPurchase(this.context, createPurchaseParam).then((result) => {
      const msg: string = 'Succeeded in creating purchase.';
      Logger.info(TAG, msg);
      promptAction.openToast({
        message:  msg,
        duration: 2000,
      });
      this.dealPurchaseData(result.purchaseData);
    }).catch((err: BusinessError) => {
      const msg: string = `Failed to create purchase. Code is ${err.code}, message is ${err.message}`;
      Logger.error(TAG, msg);
      promptAction.openToast({
        message:  msg,
        duration: 2000,
      });
      if (err.code === iap.IAPErrorCode.PRODUCT_OWNED || err.code === iap.IAPErrorCode.SYSTEM_ERROR) {
        this.queryPurchases();
      }
    })
  } 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);
    promptAction.openToast({
      message:  msg,
      duration: 2000,
    });
  }
}

查询未消耗票据的订单

javascript 复制代码
async queryPurchases(): Promise<void> {
  return new Promise<void>((resolve) => {
    const param: iap.QueryPurchasesParameter = {
      productType: iap.ProductType.CONSUMABLE,
      queryType: iap.PurchaseQueryType.UNFINISHED
    };
    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();
    });
  });
​
}
  • 解密票据信息
typescript 复制代码
dealPurchaseData(purchaseData: string) {
  try {
    // You are advised to send purchaseData to the app server for signature verification.
    const jwsPurchaseOrder = (JSON.parse(purchaseData) as PurchaseData).jwsPurchaseOrder;
    if (!jwsPurchaseOrder) {
      Logger.error(TAG, 'dealPurchaseData, jwsPurchaseOrder invalid');
      return;
    }
    // Decode jwsPurchaseOrder and perform signature verification.
    const purchaseOrderStr = JWSUtil.decodeJwsObj(jwsPurchaseOrder);
    const purchaseOrderPayload = JSON.parse(purchaseOrderStr) as PurchaseOrderPayload;
    // If the verification is successful, deliver the product.
    // ...
    // After the delivery is successful, send a finishPurchase request to IAP Kit to acknowledge the delivery
    // and complete the purchase.
    if (purchaseOrderPayload && purchaseOrderPayload.finishStatus !== FinishStatus.FINISHED) {
      this.finishPurchase(purchaseOrderPayload);
    }
  } catch (e) {
    Logger.error(TAG, 'dealPurchaseData json error');
  }
}

JWSUtil 工具类

typescript 复制代码
import { util } from '@kit.ArkTS';
import Logger from './Logger';
​
const TAG: string = 'JWSUtil';
const BASE64_PADDING_MOD: number = 4;
const BASE64_PADDING_INVALID: number = 1;
​
export class JWSUtil {
  public static decodeJwsObj(data: string): string {
    const jws: string[] = data.split('.');
    let result: string = '';
    if (jws.length < 3) {
      return result;
    }
    try {
      const textDecoder = util.TextDecoder.create('utf-8', { ignoreBOM: true });
      const base64 = new util.Base64Helper();
      let payload = jws[1];
      const pad = payload.length % BASE64_PADDING_MOD;
      if (pad) {
        if (pad === BASE64_PADDING_INVALID) {
          throw new Error('InvalidLengthError: Input base64 string is the wrong length to determine padding');
        }
        payload += new Array(BASE64_PADDING_MOD - pad + 1).join('=');
      }
      result = textDecoder.decodeToString(base64.decodeSync(payload));
    } catch (err) {
      Logger.error(TAG, `decodeJwsObj parse err: ${JSON.stringify(err)}`);
    }
    return result;
  }
}

消耗订单

javascript 复制代码
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}`);
  });
}

补单机制

我们在每次应用重新登录和每次支付之前都需要调用 queryPurchases 去查询商品未消耗的订单

javascript 复制代码
async queryPurchases(): Promise<void> {
  return new Promise<void>((resolve) => {
    const param: iap.QueryPurchasesParameter = {
      productType: iap.ProductType.CONSUMABLE,
      queryType: iap.PurchaseQueryType.UNFINISHED
    };
    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();
    });
  });
​
}

我们也要使用关系型数据库存储我们 purchaseOrderId 订单号 purchaseToken 票据 productId商品id , 然后通过透传的订单号来

查询本地数据库,然后把因为弱网或者崩溃导致掉单的情况补单给用户补发货。

测试支付注意事项

  • 完成应用开发准备。其中配置签名信息时,请使用手动签名方式。详情请参见:

    应用开发准备

    如果开发者应用的compatibleSdkVersion>=14,则接入IAP Kit不要求开发者添加公钥指纹以及配置应用身份信息

  • 开通商户服务。详情请参见:开通商户服务

  • 开启和激活应用内购买服务。详情请参见:开启和激活应用内购买服务

  • 在AppGallery Connect中添加商品信息。详情请参见:配置商品信息

  • 配置示例代码:

    • "AppScope/app.json5"文件中的bundleName修改为您自己应用的包名。
    • 替换"entry/src/main/module.json5"文件中的client_id,详情请参见:配置应用身份信息
    • 将本demo中的商品替换为您的商品(替换iap.queryProducts接口参数中productIds字段的商品ID,注意商品ID和商品的类型要匹配)。

最后总结

鸿蒙next 的IAP Kit 内购支付,和安卓的内购和ios内购支付的接入流程很类似,我们需要注意就是处理补单逻辑,主要就是要处理弱网和崩溃的情况需要在应用重新启动的登录后去查询本地数据库和鸿蒙给出查询未消耗的订单。然后把核心票据 商品id 支付订单号 提交给服务器 去补发货。如果你是第一次接入就按照次文档或者官方最新文档接入即可 最后呢 希望我都文章能帮助到各位同学工作和学习 如果你觉得文章还不错麻烦给我三连 关注点赞和转发 谢谢

相关推荐
simple_lau2 小时前
鸿蒙开发中的弹窗方案对比
harmonyos·arkts·arkui
li理2 小时前
鸿蒙 ArkTS 状态管理全解析:从基础到实战,轻松掌握响应式开发
harmonyos
前端世界16 小时前
鸿蒙分布式任务调度深度剖析:跨设备并行计算的最佳实践
分布式·华为·harmonyos
whysqwhw20 小时前
鸿蒙组件间通讯
harmonyos
AlbertZein21 小时前
HarmonyOS5 源码分析 —— 生命周期与状态管理(2)
架构·harmonyos
长弓三石1 天前
鸿蒙网络编程系列61-仓颉版基于TCP实现最简单的HTTP服务器
网络·harmonyos·鸿蒙·仓颉
zhanshuo2 天前
鸿蒙权限管理全攻略:从声明到动态申请的实战指南
harmonyos
zhanshuo2 天前
鸿蒙分布式任务调度深度剖析:跨设备并行计算的最佳实践
harmonyos
无风听海2 天前
HarmonyOS之app.json5功能详解
harmonyos·app.json5