storekit
应用内购买与订阅(StoreKit 2)。
应用内购买与订阅(StoreKit 2):加载商品、发起购买、恢复购买、查询订阅状态。
边界:需要 App Store Connect 配置商品 ID,并使用带 In-App Purchase 能力的签名构建;沙盒账号在真机测试。purchase()会弹出系统购买面板;用户取消返回result: "cancelled",不抛异常。购买、恢复、管理订阅放在按钮回调,不要在body()中调用。
#模块概览
| 项 | 说明 |
|---|---|
| 导入 | import storekit |
| 适合做什么 | 专业版解锁、订阅会员、恢复已购权益 |
| 调用时机 | 用户点击购买/恢复;先 load_products 展示价格 |
| 推荐顺序 | load_products → 展示商品 → purchase → subscription_status |
| 用户取消 | purchase 返回 {"result": "cancelled"},需单独处理 |
#快速开始
下面脚本加载商品元数据并打印价格:
已复制
import storekit
PRODUCT_ID = "com.yourapp.pro" # 换成你在 App Store Connect 创建的商品 ID
try:
products = storekit.load_products([PRODUCT_ID])
for p in products:
print(p["display_name"], p["price"], p["type"])
except storekit.StoreKitError as exc:
print("商品加载失败:", exc, f"code={exc.code}")
发起购买并处理结果:
已复制
import storekit
result = storekit.purchase(PRODUCT_ID)
if result.get("result") == "purchased":
print("购买成功:", result.get("transaction_id"))
elif result.get("result") == "cancelled":
print("用户取消")
elif result.get("result") == "pending":
print("待处理(如家长批准)")
else:
print("购买未完成:", result)
#AppUI 示例
加载、购买、恢复都放在按钮回调;取消时保持页面可操作。
已复制
import appui
import storekit
PRODUCT_ID = "com.yourapp.pro" # 换成你的商品 ID
state = appui.State(
product_name="—",
product_price="—",
product_type="—",
subs="—",
status="点击加载商品",
)
def load_product():
try:
products = storekit.load_products([PRODUCT_ID])
if not products:
state.batch_update(
product_name="—",
product_price="—",
product_type="—",
status="未找到商品(检查商品 ID 与签名配置)",
)
return
p = products[0]
state.batch_update(
product_name=p.get("display_name", "—"),
product_price=p.get("price", "—"),
product_type=p.get("type", "—"),
status="商品已加载",
)
except storekit.StoreKitError as exc:
state.status = f"加载失败: {exc}"
def buy_product():
try:
result = storekit.purchase(PRODUCT_ID) or {}
outcome = result.get("result", "unknown")
if outcome == "purchased":
state.status = f"购买成功 · tx={result.get('transaction_id', '—')}"
refresh_subs()
elif outcome == "cancelled":
state.status = "用户取消购买"
elif outcome == "pending":
state.status = "购买待处理"
else:
state.status = f"购买未完成: {outcome}"
except storekit.StoreKitError as exc:
state.status = f"购买失败: {exc}"
def restore_purchases():
try:
restored = storekit.restore() or []
state.status = f"已恢复 {len(restored)} 项" if restored else "无可恢复购买"
refresh_subs()
except storekit.StoreKitError as exc:
state.status = f"恢复失败: {exc}"
def refresh_subs():
try:
subs = storekit.subscription_status() or []
if not subs:
state.subs = "无活跃订阅"
return
lines = []
for s in subs:
pid = s.get("product_id", "?")
active = "有效" if s.get("is_active") else "失效"
lines.append(f"{pid} · {active}")
state.subs = " · ".join(lines)
except storekit.StoreKitError as exc:
state.subs = f"查询失败: {exc}"
def manage_subs():
try:
storekit.show_manage_subscriptions()
state.status = "已打开系统订阅管理"
except storekit.StoreKitError as exc:
state.status = f"无法打开: {exc}"
def body():
return appui.NavigationStack(
appui.Form([
appui.Section("商品", [
appui.LabeledContent("名称", value=state.product_name),
appui.LabeledContent("价格", value=state.product_price),
appui.LabeledContent("类型", value=state.product_type),
]),
appui.Section("订阅状态", [
appui.Text(state.subs).font("caption"),
]),
appui.Section("操作", [
appui.Button("加载商品", action=load_product),
appui.Button("购买", action=buy_product)
.button_style("bordered_prominent"),
appui.Button("恢复购买", action=restore_purchases),
appui.Button("刷新订阅", action=refresh_subs),
appui.Button("管理订阅", action=manage_subs),
appui.Text(state.status).foreground_color("secondaryLabel"),
], footer="需真机 + 沙盒账号 + 有效商品 ID。"),
]).navigation_title("内购")
)
appui.run(body, state=state)
#API 参考
#速查
| API | 作用 |
|---|---|
load_products(identifiers) | 加载商品元数据列表 |
purchase(product_id) | 发起购买 → {result, transaction_id} |
restore() | 恢复购买 → 商品 ID 列表 |
subscription_status() | 当前订阅权益列表 |
show_manage_subscriptions() | 打开系统订阅管理页 |
StoreKitError | Bridge/网络/校验失败时抛出 |
#load_products
load_products(identifiers) -> list[dict]
已复制
products = storekit.load_products(["com.app.monthly", "com.app.yearly"])
每个商品字典:
| 字段 | 说明 |
|---|---|
id | 商品 ID |
display_name | 显示名称 |
description | 描述 |
price | 本地化价格字符串(如 ¥12.00) |
type | subscription / non_consumable / consumable |
列表为空表示 ID 无效或未在 App Store Connect 配置。
#purchase
purchase(product_id) -> dict
已复制
result = storekit.purchase("com.yourapp.pro")
result | 含义 |
|---|---|
purchased | 购买成功;transaction_id 有值 |
cancelled | 用户取消(非异常) |
pending | 待处理(如 Ask to Buy) |
failed | 其他失败 |
成功交易会在 Bridge 内 finish();无需 Python 侧再调完成接口。
#restore
restore() -> list[str] — 遍历当前有效权益,返回已恢复的商品 ID 列表。
#subscription_status
subscription_status() -> list[dict]
已复制
for sub in storekit.subscription_status():
print(sub["product_id"], sub["is_active"], sub.get("expiration_date"))
| 字段 | 说明 |
|---|---|
product_id | 订阅商品 ID |
is_active | 是否仍有效 |
expiration_date | 过期时间 Unix 时间戳,或 null |
#show_manage_subscriptions
show_manage_subscriptions() — 打开系统「管理订阅」界面(iOS 15+)。
#异常
code | 含义 |
|---|---|
invalid_input | 缺少商品 ID |
not_found | 商品不存在 |
verification_failed | 交易校验失败 |
timeout | 异步操作超时 |
#常见错误
| 错误写法 | 后果 | 修正 |
|---|---|---|
在 body() 里 purchase() | 刷新时重复弹购买 | 放进按钮回调 |
| 把用户取消当异常 | 误判为崩溃 | 检查 result == "cancelled" |
| 商品 ID 写错 | load_products 返回空 | 对照 App Store Connect |
| 模拟器未登录沙盒 | 加载/购买失败 | 真机 + 沙盒测试账号 |
| 未配置 IAP 能力 | Bridge 不可用 | 签名构建开启 In-App Purchase |
#相关文档
#预期效果
运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。