045-IAP Kit应用内支付
📅 2026/7/19 19:35:37
👁️ 阅读次数
📝 编程学习
045 — IAP Kit 应用内支付:会员订阅全流程实现
简介
付费会员是记账应用实现商业闭环的重要途径。MoneyTrack 通过 IAP Kit 实现了完整的应用内购买功能,支持自动续费订阅(AUTORENEWABLE)和非续费订阅(NONRENEWABLE)两种商品类型。从商品信息配置、购买流程发起、收银台拉起、收据验签到会员状态管理,形成了一条完整的支付链路。MemberSubscriptionPage 作为订阅入口页面,通过 ProductInfo 配置商品列表,结合 JWSUtil 对支付平台返回的 JWS 收据进行解码验证。
完整购买流程
IAP Kit 的完整购买流程分为五个关键步骤:
商品配置(ProductInfo 完整定义)
商品信息通过iap.ProductInfo对象定义,在查询商品时由系统返回:
import{iap}from'@kit.IAPKit';// 查询商品信息asyncfunctionqueryProducts():Promise<iap.ProductInfo[]>{constproductIds=['moneytrack_monthly','moneytrack_yearly'];constresult=awaitiap.queryProductInfo({productIds:productIds,type:iap.ProductType.AUTORENEWABLE});returnresult.productInfoList;}// ProductInfo 包含以下完整字段:interfaceProductInfo{productId:string;// 商品唯一 ID(在 AGC 控制台配置)type:iap.ProductType;// 商品类型title:string;// 商品名称(本地化)description:string;// 商品描述price:string;// 显示价格(如 "¥12.00")microPrice:number;// 微单位价格(如 1200,单位:分)currency:string;// 货币代码(如 "CNY")status:number;// 商品状态(0=正常,1=下架)subscriptionPeriod:string;// 订阅周期(如 "P1M" 表示每月)freeTrialPeriod:string;// 免费试用期(如 "P7D" 表示 7 天)}订阅状态管理
会员订阅不是一次性操作,需要持续管理会员状态、检测过期、处理续费:
import{iap}from'@kit.IAPKit';exportclassMembershipManager{// 检测当前会员状态staticasynccheckMembershipStatus():Promise<MembershipStatus>{try{constreceipts=awaitiap.getPurchaseRecords({type:iap.ProductType.AUTORENEWABLE});if(receipts.length===0){return{isActive:false,level:'none',expireDate:null};}// 取最新的收据constlatestReceipt=receipts[0];constdecoded=JWSUtil.decode(latestReceipt.receipt);constpayload=decoded.payload;constexpireDate=newDate(payload.expirationDate);constnow=newDate();return{isActive:expireDate>now,level:this.getProductLevel(payload.productId),expireDate:expireDate,autoRenew:payload.autoRenewStatus===1,daysRemaining:Math.ceil((expireDate.getTime()-now.getTime())/86400000)};}catch(error){console.error('Check membership failed:',error);return{isActive:false,level:'none',expireDate:null};}}// 处理续费提醒staticasynchandleRenewReminder(){conststatus=awaitthis.checkMembershipStatus();if(status.isActive&&status.daysRemaining<=7&&status.daysRemaining>0){// 会员即将过期,推送续费提醒this.showRenewPrompt(status.daysRemaining);}elseif(!status.isActive&&status.level!=='none'){// 会员已过期,引导重新订阅this.showExpiredPrompt();}}// 恢复购买(跨设备同步)staticasyncrestorePurchases(){constreceipts=awaitiap.getPurchaseRecords({type:iap.ProductType.AUTORENEWABLE});// 将收据列表发送到服务端验证并激活for(constreceiptofreceipts){awaitthis.verifyOnServer(receipt.receipt);}}}支付安全(JWS 解码验签与服务端验证)
支付安全是应用内购买的核心。IAP Kit 返回的收据采用 JWS(JSON Web Signature)格式,需要在服务端完成签名验证后才能激活会员权益:
// 客户端:解析 JWS 收据exportclassJWSUtil{// JWS 格式为:header.payload.signaturestaticdecode(jwsString:string):DecodedJWS{constparts=jwsString.split('.');if(parts.length!==3){thrownewError('Invalid JWS format');}return{header:JSON.parse(this.base64UrlDecode(parts[0])),payload:JSON.parse(this.base64UrlDecode(parts[1])),signature:parts[2],// 客户端仅解析,验证由服务端完成verify():boolean{returnfalse;// 客户端不做签名验证}};}privatestaticbase64UrlDecode(str:string):string{// Base64 URL 解码实现constbase64=str.replace(/-/g,'+').replace(/_/g,'/');returndecodeURIComponent(Array.from(atob(base64),c=>'%'+('00'+c.charCodeAt(0).toString(16)).slice(-2)).join(''));}}// 服务端验证逻辑(Node.js 示例)/* async function verifyReceipt(receipt: string) { // 1. 获取 IAP Kit 公钥(从华为 IAP 服务端获取) const publicKey = await getIAPPublicKey(); // 2. 使用 JWS 库验证签名 const jws = jose.generalVerify(receipt, publicKey); // 3. 验证 payload 中的关键字段 const payload = jws.payload; if (payload.productId !== expectedProductId) throw new Error('Product mismatch'); if (payload.purchaseTime + 3600000 < Date.now()) throw new Error('Receipt expired'); // 4. 检查 purchaseToken 是否已被使用(防止重放攻击) if (await isTokenUsed(payload.purchaseToken)) throw new Error('Token already used'); // 5. 标记 token 已使用并激活会员 await markTokenUsed(payload.purchaseToken); await activateMembership(payload); } */安全要点说明:客户端可以解析 JWS 查看收据内容,但绝不能仅凭客户端解析结果就激活会员。签名验证必须在自有服务端完成,以防止伪造收据。服务端还需校验purchaseToken的唯一性,防止重放攻击。
最佳实践
- 服务端强制验签:所有会员激活逻辑必须在服务端完成,客户端仅做展示和收据透传。
- 支持恢复购买:提供"恢复购买"按钮,通过
getPurchaseRecords获取历史收据,确保用户在不同设备间已购权益同步。 - 处理收据重复回调:
purchase操作可能因网络原因收到重复回调,需通过purchaseToken去重。 - 订阅状态本地缓存:定期检测会员状态并缓存到本地,避免每次打开应用都调用网络接口。
- 提前 7 天续费提醒:在会员到期前 7 天开始提示续费,降低流失率。
- 测试商品与正式商品隔离:开发阶段使用 AGC 控制台配置的测试商品(价格为零),上架前切换为正式商品。
项目代码案例
文件路径:feature_membership/ProductInfo.ets
import{iap}from'@kit.IAPKit';exportconstPRODUCTS:iap.ProductInfo[]=[{productId:'moneytrack_monthly',type:iap.ProductType.AUTORENEWABLE,title:'月度会员',description:'解锁所有高级功能,按月订阅',price:'¥12.00',microPrice:1200,currency:'CNY',status:0,subscriptionPeriod:'P1M',freeTrialPeriod:'P7D'},{productId:'moneytrack_yearly',type:iap.ProductType.AUTORENEWABLE,title:'年度会员',description:'解锁所有高级功能,按年订阅享优惠',price:'¥98.00',microPrice:9800,currency:'CNY',status:0,subscriptionPeriod:'P1Y',freeTrialPeriod:'P7D'}];文件路径:feature_membership/MemberSubscriptionPage.ets
import{iap}from'@kit.IAPKit';asyncfunctionstartPurchase(productId:string){try{// 1. 创建订单constpurchaseResult=awaitiap.createPurchase({productId});// 2. 拉起收银台awaitiap.purchase(purchaseResult);// 3. 获取 JWS 收据constjwsReceipt=purchaseResult.inAppPurchaseResponse.receipt;// 4. 客户端解码查看constdecoded=JWSUtil.decode(jwsReceipt);console.info('Purchase payload:',decoded.payload);// 5. 发送到服务端验证constverified=awaitverifyOnServer(jwsReceipt);if(verified){awaitactivateLocalMembership(decoded.payload);showSuccessPrompt();}}catch(error){console.error('Purchase failed:',error);showErrorPrompt(error.message);}}推荐参考文档
- IAP Kit 应用内支付指南
- IAP Kit 服务端验签指南
- JWS 解码与验签规范
编程学习
技术分享
实战经验