PythonIDE Docs
中文
简体中文

storekit

应用内购买与订阅(StoreKit 2)。

应用内购买与订阅(StoreKit 2):加载商品、发起购买、恢复购买、查询订阅状态。

边界:需要 App Store Connect 配置商品 ID,并使用带 In-App Purchase 能力的签名构建;沙盒账号在真机测试。purchase() 会弹出系统购买面板;用户取消返回 result: "cancelled"不抛异常。购买、恢复、管理订阅放在按钮回调,不要在 body() 中调用。

#模块概览

说明
导入import storekit
适合做什么专业版解锁、订阅会员、恢复已购权益
调用时机用户点击购买/恢复;先 load_products 展示价格
推荐顺序load_products → 展示商品 → purchasesubscription_status
用户取消purchase 返回 {"result": "cancelled"},需单独处理

#快速开始

下面脚本加载商品元数据并打印价格:

python
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}")

发起购买并处理结果:

python
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 示例

加载、购买、恢复都放在按钮回调;取消时保持页面可操作。

python
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()打开系统订阅管理页
StoreKitErrorBridge/网络/校验失败时抛出

#load_products

load_products(identifiers) -> list[dict]

python
products = storekit.load_products(["com.app.monthly", "com.app.yearly"])

每个商品字典:

字段说明
id商品 ID
display_name显示名称
description描述
price本地化价格字符串(如 ¥12.00
typesubscription / non_consumable / consumable

列表为空表示 ID 无效或未在 App Store Connect 配置。

#purchase

purchase(product_id) -> dict

python
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]

python
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

#相关文档

文档用途
keychain存储用户令牌(非交易凭证)
dialogs购买前确认提示
原生能力入口MiniApp 场景配方

#预期效果

运行示例后,界面应出现文档描述的目标结果;若与预期不符,先看「失败路径」并按返回值或日志排查。