更新记录
0.1.0(2026-08-06)
基于 StoreKit 2 的 iOS 内购插件,callback 风格 API,并提供等价的 Promise 封装(core/storekit.api.js)。
- 商品查询:
getProducts - 购买与完成:
purchase/finishTransaction - 交易更新监听:
startObserving/stopObserving(续订 / 退款 / Ask-to-Buy / 跨设备购买 / 启动补单) - 权益与状态查询:
getCurrentEntitlements/getLatestTransaction/getSubscriptionStatus/getUnfinishedTransactions - 恢复购买:
restorePurchases - 系统页:
showManageSubscriptions/presentCodeRedemptionSheet
平台兼容性
uni-app(5.0)
| Vue2 | Vue2插件版本 | Vue3 | Vue3插件版本 | Chrome | Safari | app-vue | app-nvue | Android | iOS | iOS插件版本 | 鸿蒙 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| √ | 0.1.0 | √ | 0.1.0 | - | - | - | - | - | 15 | 0.1.0 | - |
| 微信小程序 | 支付宝小程序 | 抖音小程序 | 百度小程序 | 快手小程序 | 京东小程序 | 鸿蒙元服务 | QQ小程序 | 飞书小程序 | 小红书小程序 | 快应用-华为 | 快应用-联盟 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| - | - | - | - | - | - | - | - | - | - | - | - |
uni-app x(5.0)
| Chrome | Safari | Android | iOS | iOS插件版本 | 鸿蒙 | 微信小程序 |
|---|---|---|---|---|---|---|
| - | - | - | 15 | 0.1.0 | - | - |
其他
| 多语言 | 暗黑模式 | 宽屏模式 |
|---|---|---|
| √ | √ | √ |
dh-storekit
基于 StoreKit 2 的 iOS 应用内购买(IAP)插件,支持消耗型、非消耗型、自动续订订阅。每笔交易携带 Apple 签名的 JWS,配合服务端 App Store Server API / Server Notifications 完成验签与对账。
- 平台:iOS 15+
- 商品类型:消耗型 / 非消耗型 / 自动续订订阅 / 非续订订阅
- API 风格:默认 callback(
success/fail/complete);另提供可选的 Promise 封装扩展
背景
uni-app 内置的 appleiap 基于 StoreKit 1。在自动续订订阅的一个典型场景下会出问题:
用户订阅某商品 → 取消 → 到期 → 再次购买同一商品。
此时调用 requestPayment 不会真正发起支付——不弹密码 / Face ID、不扣款,却直接返回成功;返回的 transactionIdentifier 与 transactionReceipt 都是上一笔旧交易的数据,Apple 服务器侧根本不存在这笔新交易。服务端拿这份陈旧数据去验签,验到的是早已过期的旧交易,结果只能是失败:用户以为订阅成功,实际既没扣款也没拿到权益。(已通过 App Store Server API 反查确认:该场景下 Apple 侧无对应新交易。)
dh-storekit 改用 StoreKit 2 从机制上根治:
purchase()要么真正走完支付流程、产生一笔由 Apple 当场签发的、带签名 JWS 的新交易,要么明确返回userCancelled/pending——不存在「未支付却伪造成功、回传旧交易」的情况;transactionId与签名凭证天生属于同一笔交易,服务端直接用 JWS / App Store Server API 验签,不依赖会滞后的 receipt;- 续订、退款、Ask-to-Buy、跨设备购买通过
Transaction.updates实时送达,补单不丢。
特性
- 商品查询、购买、完成交易(finish)
- 自动续订订阅:通过
startObserving实时接收续订、退款、Ask-to-Buy 审批通过、跨设备购买等交易更新 - 当前权益、最近交易、订阅状态、未完成交易查询
- 恢复购买、系统「管理订阅」页、兑换码弹窗
- 每笔交易提供签名 JWS(
transaction.jws)用于服务端验签 appAccountToken透传,服务端可回读以绑定业务用户
引入
默认 callback 风格,按需引入所用方法:
import { isSupported, startObserving, getProducts, purchase, finishTransaction } from '@/uni_modules/dh-storekit';
如偏好 async/await,可改用可选的 Promise 封装扩展(详见文末「Promise 风格」):
import StoreKit from '@/uni_modules/dh-storekit/core/storekit.api.js';
API 参考
数据方法为
方法名(options)形式,options内含success/fail/complete三个可选回调,方法本身返回void。 下方「Promise 风格」章节提供等价的Promise版签名。
isSupported()
检测当前环境是否支持本插件。
- 参数:无
- 返回:
boolean—— iOS 15+ 返回true,否则false
if (isSupported()) {
/* ... */
}
startObserving(onUpdate)
注册交易更新监听。应在 App 启动后尽早调用一次。自动续订续期、退款、Ask-to-Buy 审批通过、其他设备购买、以及启动时遗留的未完成交易,都会通过此回调送达。每收到一笔,验签入库后应调用 finishTransaction 完成它。
-
参数
参数 类型 说明 onUpdate(tx: SK2Transaction) => void每笔交易更新回调一次 -
返回:
void
startObserving((tx) => {
uploadToServer(tx.jws);
finishTransaction({ transactionId: tx.transactionId });
});
stopObserving()
停止交易更新监听。
- 参数:无
- 返回:
void
getProducts(options)
向 App Store 查询商品信息(名称、本地化价格、订阅周期等)。
-
参数
字段 类型 必填 说明 productIdsstring[]是 App Store Connect 中配置的商品 ID 列表 success(res: { products: SK2Product[] }) => void否 成功回调 fail(err: StoreKitFail) => void否 失败回调 complete() => void否 成功或失败后均回调 -
success 返回:
{ products: SK2Product[] }
getProducts({
productIds: ['com.app.monthly', 'com.app.yearly'],
success: (res) => {
console.log(res.products);
},
fail: (err) => {
console.log(err.errCode, err.errMsg);
},
});
purchase(options)
发起购买,唤起系统购买面板。
-
参数
字段 类型 必填 说明 productIdstring是 商品 ID appAccountTokenstring \| null否 业务用户标识,须为 UUID 字符串;写入交易,服务端验签时可回读 success(res: SK2PurchaseResult) => void否 成功回调(含取消、待处理等状态) fail(err: StoreKitFail) => void否 失败回调 complete() => void否 成功或失败后均回调 -
success 返回:
SK2PurchaseResult,其中state取值:state 含义 success购买成功, res.transaction为本次交易pending待外部动作(如家长 Ask-to-Buy 审批、银行验证),结果稍后经 startObserving送达userCancelled用户取消 unverified交易本地验签未通过,建议交服务端二次验签判断
purchase({
productId: 'com.app.monthly',
appAccountToken: '5C2D6A1E-....',
success: (res) => {
if (res.state === 'success') {
uploadToServer(res.transaction.jws);
finishTransaction({ transactionId: res.transaction.transactionId });
}
},
fail: (err) => {
console.log(err.errCode, err.errMsg);
},
});
finishTransaction(options)
完成(finish)一笔交易,将其从交易队列移除。应在服务端验签并发放权益后调用;未完成的交易会在每次启动时经 startObserving 重新送达。
-
参数
字段 类型 必填 说明 transactionIdstring是 交易号 success() => void否 成功回调(无参数) fail(err: StoreKitFail) => void否 失败回调 complete() => void否 完成回调 -
success 返回:无参数
getCurrentEntitlements(options)
查询用户当前有效权益:未过期的订阅 + 已购买的非消耗型商品。常用于 App 启动时校准本地权益状态。
- 参数:
{ success?: (res: { transactions: SK2Transaction[] }) => void, fail?, complete? } - success 返回:
{ transactions: SK2Transaction[] }
getLatestTransaction(options)
查询指定商品的最近一笔交易。
-
参数
字段 类型 必填 说明 productIdstring是 商品 ID success(tx: SK2Transaction \| null) => void否 成功回调,无交易时为 nullfail(err: StoreKitFail) => void否 失败回调 complete() => void否 完成回调 -
success 返回:
SK2Transaction | null
getSubscriptionStatus(options)
查询某商品所属订阅组的状态(是否订阅中、是否自动续订、将续订的商品等)。
-
参数
字段 类型 必填 说明 productIdstring是 订阅商品 ID success(res: { statuses: SK2SubscriptionStatus[] }) => void否 成功回调 fail(err: StoreKitFail) => void否 失败回调 complete() => void否 完成回调 -
success 返回:
{ statuses: SK2SubscriptionStatus[] }
getUnfinishedTransactions(options)
查询所有未完成(未 finish)的交易。通常无需手动调用——startObserving 已会在启动时送达这些交易;此方法用于需要主动拉取的场景。
- 参数:
{ success?: (res: { transactions: SK2Transaction[] }) => void, fail?, complete? } - success 返回:
{ transactions: SK2Transaction[] }
restorePurchases(options)
恢复购买。强制与 App Store 同步交易,会弹出 Apple 账号登录。用于「恢复购买」按钮。
- 参数:
{ success?: (res: { transactions: SK2Transaction[] }) => void, fail?, complete? } - success 返回:
{ transactions: SK2Transaction[] }(同步后的当前权益)
showManageSubscriptions(options)
打开系统「管理订阅」页面,用户可在此退订或更换订阅档位。
- 参数:
{ success?: () => void, fail?: (err: StoreKitFail) => void, complete?: () => void } - success 返回:无参数
presentCodeRedemptionSheet()
唤起兑换码 / 优惠码输入面板。
- 参数:无
- 返回:
void
Promise 风格
core/storekit.api.js 提供与上述 callback API 等价的 Promise 封装,数据方法返回 Promise,失败时 reject 出 StoreKitFail。
import StoreKit from '@/uni_modules/dh-storekit/core/storekit.api.js';
try {
const { products } = await StoreKit.getProducts(['com.app.monthly']);
const res = await StoreKit.purchase({ productId: 'com.app.monthly', appAccountToken: uuid });
if (res.state === 'success') {
await uploadToServer(res.transaction.jws);
await StoreKit.finishTransaction(res.transaction.transactionId);
}
} catch (err) {
console.log(err.errCode, err.errMsg);
}
// 事件监听仍为回调
StoreKit.startObserving((tx) => {
/* ... */
});
| 方法 | 签名 |
|---|---|
isSupported() |
boolean |
startObserving(onUpdate) |
void |
stopObserving() |
void |
getProducts(productIds) |
Promise<{ products: SK2Product[] }> |
purchase(options) |
Promise<SK2PurchaseResult> |
finishTransaction(transactionId) |
Promise<void> |
getCurrentEntitlements() |
Promise<{ transactions: SK2Transaction[] }> |
getLatestTransaction(productId) |
Promise<SK2Transaction \| null> |
getSubscriptionStatus(productId) |
Promise<{ statuses: SK2SubscriptionStatus[] }> |
getUnfinishedTransactions() |
Promise<{ transactions: SK2Transaction[] }> |
restorePurchases() |
Promise<{ transactions: SK2Transaction[] }> |
showManageSubscriptions() |
Promise<void> |
presentCodeRedemptionSheet() |
void |
数据结构
SK2Product
| 字段 | 类型 | 说明 |
|---|---|---|
id |
string |
商品 ID |
type |
'consumable' \| 'nonConsumable' \| 'autoRenewable' \| 'nonRenewable' |
商品类型 |
displayName |
string |
本地化名称 |
desc |
string |
本地化描述 |
displayPrice |
string |
本地化已格式化价格,如 ¥28.00 |
price |
string |
数值价格(字符串避免精度丢失),如 28 |
currencyCode |
string |
货币代码 |
subscriptionGroupId |
string \| null |
订阅组 ID(仅自动续订) |
subscriptionPeriod |
SK2SubscriptionPeriod \| null |
订阅周期(仅自动续订) |
SK2SubscriptionPeriod
| 字段 | 类型 | 说明 |
|---|---|---|
unit |
'day' \| 'week' \| 'month' \| 'year' |
周期单位 |
value |
number |
周期数量 |
SK2Transaction
| 字段 | 类型 | 说明 |
|---|---|---|
transactionId |
string |
本次交易号 |
originalTransactionId |
string |
原始交易号(订阅生命周期稳定主键) |
productId |
string |
商品 ID |
productType |
SK2ProductType |
商品类型 |
purchaseDate |
number |
购买时间(毫秒时间戳) |
originalPurchaseDate |
number |
首次购买时间(毫秒) |
expirationDate |
number \| null |
订阅到期时间(毫秒),非订阅为 null |
quantity |
number |
购买数量 |
ownershipType |
'purchased' \| 'familyShared' |
归属类型 |
environment |
'production' \| 'sandbox' \| 'xcode' |
运行环境 |
appAccountToken |
string \| null |
购买时透传的 UUID |
isUpgraded |
boolean |
是否已被升级(换档后旧交易标记) |
revocationDate |
number \| null |
退款 / 撤销时间(毫秒),未撤销为 null |
jws |
string |
签名交易串,POST 给服务端验签 |
SK2SubscriptionStatus
| 字段 | 类型 | 说明 |
|---|---|---|
productId |
string |
商品 ID |
state |
'subscribed' \| 'expired' \| 'inBillingRetry' \| 'inGracePeriod' \| 'revoked' |
订阅状态 |
willAutoRenew |
boolean |
是否开启自动续订 |
autoRenewProductId |
string \| null |
续订将切换到的商品(换档时) |
jws |
string |
当前周期交易的签名串 |
SK2PurchaseResult
| 字段 | 类型 | 说明 |
|---|---|---|
state |
'success' \| 'pending' \| 'userCancelled' \| 'unverified' |
购买结果状态 |
transaction |
SK2Transaction \| null |
state=success 时为本次交易,否则 null |
StoreKitFail
| 字段 | 类型 | 说明 |
|---|---|---|
errCode |
number |
错误码 |
errMsg |
string |
错误描述 |
errSubject |
string |
错误来源,固定 dh-storekit |
错误码
| errCode | 含义 |
|---|---|
| 9020001 | 当前平台 / 系统不支持(需 iOS 15+) |
| 9020002 | 商品查询失败 |
| 9020003 | 商品不存在 |
| 9020007 | 购买失败(errMsg 含原始原因) |
| 9020009 | 恢复购买失败 |
| 9020010 | 系统页展示失败 |
服务端配合
- 验签:客户端上报
transaction.jws,服务端用 App Store Server Library 验签,取出transactionId、productId、expiresDate、appAccountToken等。 - 幂等入库:以
transactionId为唯一键,先落库再发放权益,重复上报幂等处理。 - 服务端通知:在 App Store Connect 配置 App Store Server Notifications V2 回调地址,Apple 主动推送续订、退款、过期等事件,作为对账权威来源。
- 绑定用户:购买时传入的
appAccountToken可在验签结果中回读,用于建立交易与业务用户的映射。
推荐客户端流程:purchase / startObserving 拿到交易 → 上报 jws → 服务端验签入库并发放权益 → 客户端 finishTransaction。
测试
- Xcode StoreKit Testing:项目配置
.storekit文件,本地模拟商品、订阅续期、退款、Ask-to-Buy 等,无需真实账号。 - Sandbox 沙盒:真机 + 沙盒账号联调,最接近真实链路。

收藏人数:
购买源码授权版(
试用
赞赏(0)
下载 368
赞赏 6
下载 12487011
赞赏 1938
赞赏
京公网安备:11010802035340号