mcpField
A
axiom-storekit-ref

axiom-storekit-ref

github.com/charleswiltgen开发1.2k181 次安装今天更新
代码仓库

关于 axiom-storekit-ref

axiom-storekit-ref 是面向 Apple 平台开发者的 StoreKit 2 API 参考技能,涵盖购买、交易与订阅相关接口。内容包括 iOS 18.4 新字段及示例,适用于实现和排查应用内购买。

主要功能

  1. 01StoreKit 2 API 查询
  2. 02商品与订阅类型说明
  3. 03购买流程与结果处理
  4. 04交易验证及权益查询
  5. 05iOS 18.4 新字段参考

适用场景

  1. 01实现应用内购买
  2. 02规划订阅架构
  3. 03排查交易处理问题

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/charleswiltgen/axiom --skill axiom-storekit-ref

安装后在 Claude Code 中输入/axiom-storekit-ref确认已加载。

详细介绍

StoreKit 2 — 完整 API 参考

概览

StoreKit 2 是 Apple 的现代应用内购买框架,提供 async/await API、自动收据验证和 SwiftUI 集成。本参考涵盖所有 API、iOS 18.4 增强功能,以及 WWDC 2025 的全面代码示例。

支持的产品类型

消耗型:

  • 可多次购买的产品
  • 示例:金币、提示、临时增益
  • 不会在新设备上恢复

非消耗型:

  • 购买一次后永久拥有的产品
  • 示例:高级功能、关卡包、移除广告
  • 必须在新设备上恢复

自动续期订阅:

  • 自动续期的订阅
  • 按订阅群组组织
  • 必须在新设备上恢复
  • 支持:免费试用、入门优惠、促销优惠、挽回优惠

非续期订阅:

  • 固定期限的订阅(不自动续期)
  • 示例:季节通行证
  • 必须在新设备上恢复
相较于 StoreKit 1 的主要改进
  • Async/Await:使用现代并发,而非代理/闭包
  • 自动验证:内置 JSON Web Signature(JWS)验证
  • 事务类型:使用强类型 Swift 类型,而非 SKPaymentTransaction
  • 测试:用于本地测试的 StoreKit 配置文件
  • SwiftUI 视图:预构建的购买界面(ProductView、SubscriptionStoreView)
  • 服务器 API:App Store Server API 和服务器通知

何时使用本参考

在以下情况下使用本参考:

  • 使用 StoreKit 2 实现应用内购买
  • 了解 iOS 18.4 新增字段(appTransactionID、offerPeriod 等)
  • 查阅特定 API 签名和参数
  • 规划订阅架构
  • 调试事务问题
  • 实现 StoreKit 视图
  • 与 App Store Server API 集成

相关技能:

  • axiom-in-app-purchases — 包含测试优先工作流和架构模式的规范技能
  • (未来:用于审查现有 IAP 代码的 iap-auditor Agent)
  • (未来:用于从头实现 IAP 的 iap-implementation Agent)

Product

概览

Product 表示在 App Store Connect 或 StoreKit 配置文件中配置的应用内购买项目。

加载产品

基本加载:

import StoreKit

let productIDs = [
    "com.app.coins_100",
    "com.app.premium",
    "com.app.pro_monthly"
]

let products = try await Product.products(for: productIDs)
来自 WWDC 2021-10114

处理缺失的产品:

let products = try await Product.products(for: productIDs)

// Check what loaded
let loadedIDs = Set(products.map { $0.id })
let missingIDs = Set(productIDs).subtracting(loadedIDs)

if !missingIDs.isEmpty {
    print("Missing products: \(missingIDs)")
    // Products not configured in App Store Connect or .storekit file
}
产品属性

基本属性:

let product: Product

product.id // "com.app.premium"
product.displayName // "Premium Upgrade"
product.description // "Unlock all features"
product.displayPrice // "$4.99"
product.price // Decimal(4.99)
product.type // .nonConsumable

产品类型枚举:

switch product.type {
case .consumable:
    // Coins, hints, boosts
case .nonConsumable:
    // Premium features, level packs
case .autoRenewable:
    // Monthly/annual subscriptions
case .nonRenewing:
    // Seasonal passes
@unknown default:
    break
}
订阅专属属性

检查产品是否为订阅:

if let subscriptionInfo = product.subscription {
    // Product is auto-renewable subscription
    let groupID = subscriptionInfo.subscriptionGroupID
    let period = subscriptionInfo.subscriptionPeriod
}

订阅期限:

let period = product.subscription?.subscriptionPeriod

switch period?.unit {
case .day:
    print("\(period?.value ?? 0) days")
case .week:
    print("\(period?.value ?? 0) weeks")
case .month:
    print("\(period?.value ?? 0) months")
case .year:
    print("\(period?.value ?? 0) years")
default:
    break
}

入门优惠:

if let introOffer = product.subscription?.introductoryOffer {
    print("Free trial: \(introOffer.period.value) \(introOffer.period.unit)")
    print("Price: \(introOffer.displayPrice)")

    switch introOffer.paymentMode {
    case .freeTrial:
        print("Free trial - no charge")
    case .payAsYouGo:
        print("Discounted price per period")
    case .payUpFront:
        print("One-time discounted price")
    @unknown default:
        break
    }
}

促销优惠:

let offers = product.subscription?.promotionalOffers ?? []

for offer in offers {
    print("Offer ID: \(offer.id)")
    print("Price: \(offer.displayPrice)")
    print("Period: \(offer.period.value) \(offer.period.unit)")
}
购买方法

使用 UI 上下文购买(iOS 18.2+):

let product: Product
let scene: UIWindowScene

let result = try await product.purchase(confirmIn: scene)
来自 WWDC 2025-241:9:32

使用选项购买:

let accountToken = UUID()

let result = try await product.purchase(
    confirmIn: scene,
    options: [
        .appAccountToken(accountToken)
    ]
)
来自 WWDC 2025-241:11:01

使用促销优惠购买(JWS 格式):

let jwsSignature: String // From your server

let result = try await product.purchase(
    confirmIn: scene,
    options: [
        .promotionalOffer(offerID: "promo_winback", signature: jwsSignature)
    ]
)
来自 WWDC 2025-241:10:55

使用自定义入门优惠资格购买:

let jwsSignature: String // From your server

let result = try await product.purchase(
    confirmIn: scene,
    options: [
        .introductoryOfferEligibility(signature: jwsSignature)
    ]
)
来自 WWDC 2025-241:10:42

SwiftUI 购买(使用 Environment):

struct ProductView: View {
    let product: Product
    @Environment(\.purchase) private var purchase

    var body: some View {
        Button("Buy \(product.displayPrice)") {
            Task {
                do {
                    let result = try await purchase(product)
                    // Handle result
                } catch {
                    print("Purchase failed: \(error)")
                }
            }
        }
    }
}
来自 WWDC 2025-241:9:50
PurchaseResult

处理购买结果:

let result = try await product.purchase(confirmIn: scene)

switch result {
case .success(let verificationResult):
    // Purchase succeeded - verify transaction
    guard let transaction = try? verificationResult.payloadValue else {
        print("Transaction verification failed")
        return
    }

    // Grant entitlement
    await grantEntitlement(for: transaction)
    await transaction.finish()

case .userCancelled:
    // User tapped "Cancel" in payment sheet
    print("User cancelled purchase")

case .pending:
    // Purchase requires action (Ask to Buy, payment issue)
    // Transaction will arrive via Transaction.updates when approved
    print("Purchase pending approval")

@unknown default:
    break
}
来自 WWDC 2025-241

Transaction

概览

Transaction 表示成功的应用内购买。包含购买元数据、产品 ID、购买日期,以及订阅的到期日期。

新增字段(iOS 18.4)

appTransactionID:

let transaction: Transaction
let appTransactionID = transaction.appTransactionID
// Unique ID for app download (same across all purchases by same Apple Account)
来自 WWDC 2025-241:4:13

offerPeriod:

if let offerPeriod = transaction.offer?.period {
    print("Offer duration: \(offerPeriod)")
    // ISO 8601 duration format (e.g., "P1M" for 1 month)
}
来自 WWDC 2025-249:3:11

advancedCommerceInfo:

if let advancedInfo = transaction.advancedCommerceInfo {
    // Only present for Advanced Commerce API purchases
    // nil for standard IAP
}
来自 WWDC 2025-241:4:42
重要属性

基本字段:

let transaction: Transaction

transaction.id // Unique transaction ID
transaction.originalID // Original transaction ID (consistent across renewals)
transaction.productID // "com.app.pro_monthly"
transaction.productType // .autoRenewable
transaction.purchaseDate // Date of purchase
transaction.appAccountToken // UUID set at purchase time (if provided)

订阅字段:

transaction.expirationDate // When subscription expires
transaction.isUpgraded // true if user upgraded to higher tier
transaction.revocationDate // Date of refund (nil if not refunded)
transaction.revocationReason // .developerIssue or .other

优惠字段:

if let offer = transaction.offer {
    offer.type // .introductory or .promotional or .code
    offer.id // Offer identifier from App Store Connect
    offer.paymentMode // .freeTrial, .payAsYouGo, .payUpFront, .oneTime
}
来自 WWDC 2025-241:8:00
当前权益

获取所有当前权益:

var purchasedProductIDs: Set<String> = []

for await result in Transaction.currentEntitlements {
    guard let transaction = try? result.payloadValue else {
        continue
    }

    // Only include non-refunded transactions
    if transaction.revocationDate == nil {
        purchasedProductIDs.insert(transaction.productID)
    }
}
来自 WWDC 2025-241

获取特定产品的权益(iOS 18.4+):

let productID = "com.app.premium"

for await result in Transaction.currentEntitlements(for: productID) {
    if let transaction = try? result.payloadValue,
       transaction.revocationDate == nil {
        // User owns this product
        return true
    }
}
来自 WWDC 2025-241:3:31

已弃用的 API(iOS 18.4):

// ❌ Deprecated in iOS 18.4
let entitlement = await Transaction.currentEntitlement(for: productID)

// ✅ Use this instead (returns sequence, handles Family Sharing)
for await result in Transaction.currentEntitlements(for: productID) {
    // ...
}
来自 WWDC 2025-241:3:31
交易历史

获取所有交易:

for await result in Transaction.all {
    guard let transaction = try? result.payloadValue else {
        continue
    }

    print("Transaction: \(transaction.productID) on \(transaction.purchaseDate)")
}

获取产品的交易:

for await result in Transaction.all(matching: productID) {
    guard let transaction = try? result.payloadValue else {
        continue
    }

    // All transactions for this product
}
交易监听器

监听实时更新(必需):

func listenForTransactions() -> Task<Void, Never> {
    Task.detached {
        for await verificationResult in Transaction.updates {
            await handleTransaction(verificationResult)
        }
    }
}

func handleTransaction(_ result: VerificationResult<Transaction>) async {
    guard let transaction = try? result.payloadValue else {
        return
    }

    // Grant or revoke entitlement
    if transaction.revocationDate != nil {
        await revokeEntitlement(for: transaction.productID)
    } else {
        await grantEntitlement(for: transaction)
    }

    // CRITICAL: Always finish transaction
    await transaction.finish()
}
来自 WWDC 2021-10114

交易来源:

  • App 内购买
  • 从 App Store 购买(推广的 IAP)
  • 优惠码兑换
  • 订阅续订
  • 家人共享交易
  • 完成的待处理购买(购买前询问)
  • 退款通知
验证

VerificationResult:

let result: VerificationResult<Transaction>

switch result {
case .verified(let transaction):
    // ✅ Transaction signed by App Store
    await grantEntitlement(for: transaction)
    await transaction.finish()

case .unverified(let transaction, let error):
    // ❌ Transaction signature invalid
    print("Unverified: \(error)")
    // DO NOT grant entitlement
    await transaction.finish() // Still finish to clear queue
}

验证内容:

  • 交易由 App Store 签名(并非欺诈)
  • 交易属于此 App(Bundle ID 匹配)
  • 交易属于此设备
完成交易

始终调用 finish():

await transaction.finish()

何时完成:

  • ✅ 向用户授予权益后
  • ✅ 存储交易收据/ID 后
  • ✅ 即使交易未经验证也要完成(以清空队列)
  • ✅ 即使交易已退款也要完成

如果不完成会发生什么:

  • 下次启动 App 时重新传递交易
  • Transaction.updates 会再次发出交易
  • 队列会随时间不断累积

AppTransaction

概述

AppTransaction 表示 App 的首次下载。可通过 AppTransaction.shared 获取。

新增字段(iOS 18.4)

appTransactionID:

let appTransaction = try await AppTransaction.shared

switch appTransaction {
case .verified(let transaction):
    let appTransactionID = transaction.appTransactionID
    // Globally unique ID for this Apple Account + app
    // Same value appears in Transaction and RenewalInfo

case .unverified(_, let error):
    print("AppTransaction verification failed: \(error)")
}
来自 WWDC 2025-241:1:42

originalPlatform:

if let appTransaction = try? await AppTransaction.shared.payloadValue {
    let platform = appTransaction.originalPlatform

    switch platform {
    case .iOS:
        print("Originally downloaded on iPhone/iPad")
    case .macOS:
        print("Originally downloaded on Mac")
    case .tvOS:
        print("Originally downloaded on Apple TV")
    case .visionOS:
        print("Originally downloaded on Vision Pro")
    @unknown default:
        break
    }
}
来自 WWDC 2025-241:2:11

注意:在 watchOS 上下载的 App 会显示 originalPlatform = .iOS

基本属性
let appTransaction: AppTransaction

appTransaction.appVersion // "1.2.3"
appTransaction.originalAppVersion // "1.0.0"
appTransaction.originalPurchaseDate // First download date
appTransaction.bundleID // "com.company.app"
appTransaction.deviceVerification // UUID for device
appTransaction.deviceVerificationNonce // Nonce for verification
使用场景

检查 App 版本:

if let appTransaction = try? await AppTransaction.shared.payloadValue {
    if appTransaction.appVersion != currentVersion {
        // Prompt user to update
    }
}
来自 WWDC 2025-241:0:51

业务模式迁移:

// Moving from paid app to free app with IAP
if appTransaction.originalPlatform == .iOS,
   appTransaction.originalPurchaseDate < migrationDate {
    // User paid for app before migration - grant premium
    await grantPremiumAccess()
}
来自 WWDC 2025-241:2:32

Product.SubscriptionInfo.RenewalInfo

概述

RenewalInfo 提供自动续订订阅状态的信息,包括是否会续订、过期原因和即将推出的优惠。

新增字段(iOS 18.4)

appTransactionID:

let renewalInfo: RenewalInfo
let appTransactionID = renewalInfo.appTransactionID
来自 WWDC 2025-241:6:40

offerPeriod:

if let offerPeriod = renewalInfo.offerPeriod {
    print("Next renewal offer period: \(offerPeriod)")
    // ISO 8601 duration (applies at next renewal)
}
来自 WWDC 2025-249:3:11

appAccountToken:

if let token = renewalInfo.appAccountToken {
    // UUID associating subscription with your server account
}
来自 WWDC 2025-241:6:56

advancedCommerceInfo:

if let advancedInfo = renewalInfo.advancedCommerceInfo {
    // Only for Advanced Commerce API subscriptions
}
来自 WWDC 2025-241:6:50
基本属性

续订状态:

let renewalInfo: RenewalInfo

renewalInfo.willAutoRenew // true if subscription will renew
renewalInfo.autoRenewPreference // Product ID customer will renew to
renewalInfo.expirationReason // Why subscription expired (if expired)

过期原因:

switch renewalInfo.expirationReason {
case .autoRenewDisabled:
    // User turned off auto-renewal
case .billingError:
    // Payment method issue
case .didNotConsentToPriceIncrease:
    // User didn't accept price increase - show win-back offer!
case .productUnavailable:
    // Product no longer available
case .unknown:
    // Unknown reason
@unknown default:
    break
}
来自 WWDC 2025-241:5:38

宽限期:

if let gracePeriodExpiration = renewalInfo.gracePeriodExpirationDate {
    // Subscription in grace period - billing issue
    // Show update payment method UI
}

价格上涨同意:

if let consentStatus = renewalInfo.priceIncreaseStatus {
    switch consentStatus {
    case .agreed:
        // User accepted price increase
    case .notYetResponded:
        // User hasn't responded - show consent UI
    @unknown default:
        break
    }
}
访问 RenewalInfo

从 SubscriptionStatus 获取:

let statuses = try await Product.SubscriptionInfo.status(for: groupID)

for status in statuses {
    switch status.renewalInfo {
    case .verified(let renewalInfo):
        print("Will renew: \(renewalInfo.willAutoRenew)")
    case .unverified(_, let error):
        print("Renewal info verification failed: \(error)")
    }
}

Product.SubscriptionInfo.Status

概述

SubscriptionStatus 表示自动续订订阅的当前状态,包括订阅是否有效、已过期、处于宽限期或处于账单重试状态。

订阅状态

状态枚举:

let status: Product.SubscriptionInfo.Status

switch status.state {
case .subscribed:
    // User has active subscription - full access

case .expired:
    // Subscription expired - show resubscribe/win-back offer

case .inGracePeriod:
    // Billing issue but access maintained - show update payment UI

case .inBillingRetryPeriod:
    // Apple retrying payment - maintain access

case .revoked:
    // Family Sharing access removed - revoke access

@unknown default:
    break
}
来自 WWDC 2025-241
获取订阅状态

获取订阅群组的状态:

let groupID = "pro_tier"

let statuses = try await Product.SubscriptionInfo.status(for: groupID)

// Find highest service level
let activeStatus = statuses
    .filter { $0.state == .subscribed }
    .max { $0.transaction.productID < $1.transaction.productID }
来自 WWDC 2025-241:6:22

获取特定交易的状态(iOS 18.4+):

let transactionID = transaction.id

let status = try await Product.SubscriptionInfo.status(for: transactionID)
来自 WWDC 2025-241:6:40

监听状态更新:

for await statuses in Product.SubscriptionInfo.Status.updates(for: groupID) {
    // Process updated statuses
    for status in statuses {
        print("Status: \(status.state)")
    }
}
状态属性
let status: Product.SubscriptionInfo.Status

status.state // .subscribed, .expired, etc.
status.transaction // VerificationResult<Transaction>
status.renewalInfo // VerificationResult<RenewalInfo>

StoreKit 视图

ProductView(iOS 17+)

基本用法:

import StoreKit

struct ContentView: View {
    let productID = "com.app.premium"

    var body: some View {
        ProductView(id: productID)
    }
}
来自 WWDC 2023-10013

使用已加载的产品:

struct ContentView: View {
    let product: Product

    var body: some View {
        ProductView(for: product)
    }
}

自定义图标:

ProductView(id: productID) {
    Image(systemName: "star.fill")
        .foregroundStyle(.yellow)
}

控件样式:

ProductView(id: productID)
    .productViewStyle(.regular)  // Default

ProductView(id: productID)
    .productViewStyle(.compact)  // Smaller

ProductView(id: productID)
    .productViewStyle(.large)  // Prominent
StoreView(iOS 17+)

基本商店:

struct ContentView: View {
    let productIDs = [
        "com.app.coins_100",
        "com.app.coins_500",
        "com.app.coins_1000"
    ]

    var body: some View {
        StoreView(ids: productIDs)
    }
}
来自 WWDC 2023-10013

使用已加载的产品:

struct ContentView: View {
    let products: [Product]

    var body: some View {
        StoreView(products: products)
    }
}
SubscriptionStoreView(iOS 17+)

基本订阅商店:

struct SubscriptionView: View {
    let groupID = "pro_tier"

    var body: some View {
        SubscriptionStoreView(groupID: groupID) {
            // Marketing content above subscription options
            VStack {
                Image("app-icon")
                Text("Go Pro")
                    .font(.largeTitle.bold())
                Text("Unlock all features")
            }
        }
    }
}
来自 WWDC 2023-10013

控件样式:

SubscriptionStoreView(groupID: groupID) {
    // Marketing content
}
.subscriptionStoreControlStyle(.automatic)    // Default
.subscriptionStoreControlStyle(.picker)       // Horizontal picker
.subscriptionStoreControlStyle(.buttons)      // Stacked buttons
.subscriptionStoreControlStyle(.prominentPicker) // Large picker (iOS 18.4+)
来自 WWDC 2025-241
SubscriptionOfferView(iOS 18.4+)

基本优惠视图:

struct ContentView: View {
    let productID = "com.app.pro_monthly"

    var body: some View {
        SubscriptionOfferView(id: productID)
    }
}
来自 WWDC 2025-241:14:27

使用已加载的产品:

let product: Product // Already loaded via Product.products(for:)

SubscriptionOfferView(product: product)

使用促销图标:

SubscriptionOfferView(
    id: productID,
    prefersPromotionalIcon: true
)

// Also available as modifier
SubscriptionOfferView(id: productID)
    .prefersPromotionalIcon(true)

使用自定义图标:

SubscriptionOfferView(id: productID) {
    Image("custom-icon")
        .resizable()
        .frame(width: 60, height: 60)
} placeholder: {
    Image(systemName: "photo")
        .foregroundStyle(.gray)
}
来自 WWDC 2025-241:15:14

使用详情操作:

@State private var showStore = false

var body: some View {
    SubscriptionOfferView(id: productID)
        .subscriptionOfferViewDetailAction {
            showStore = true
        }
        .sheet(isPresented: $showStore) {
            SubscriptionStoreView(groupID: "pro_tier")
        }
}
来自 WWDC 2025-241:15:38

可见关系:

// Only show if customer can upgrade
SubscriptionOfferView(
    groupID: "pro_tier",
    visibleRelationship: .upgrade
)

// Only show if customer can downgrade
SubscriptionOfferView(
    groupID: "pro_tier",
    visibleRelationship: .downgrade
)

// Show crossgrade options (same tier, different billing period)
SubscriptionOfferView(
    groupID: "pro_tier",
    visibleRelationship: .crossgrade
)

// Show current subscription (only if offer available)
SubscriptionOfferView(
    groupID: "pro_tier",
    visibleRelationship: .current
)

// Show any plan in group
SubscriptionOfferView(
    groupID: "pro_tier",
    visibleRelationship: .all
)
来自 WWDC 2025-241:17:44

使用 App 图标:

SubscriptionOfferView(
    groupID: groupID,
    visibleRelationship: .all,
    useAppIcon: true
)
来自 WWDC 2025-241:19:06
优惠修饰符

促销优惠(JWS):

SubscriptionStoreView(groupID: groupID)
    .subscriptionPromotionalOffer(
        for: { subscription in
            // Return offer for this subscription
            return subscription.promotionalOffers.first
        },
        signature: { subscription, offer in
            // Get JWS signature from server
            let signature = try await server.signOffer(
                productID: subscription.id,
                offerID: offer.id
            )
            return signature
        }
    )
来自 WWDC 2025-241:12:17
subscriptionStatusTask Modifier(iOS 18.4+)

使用 SwiftUI modifier 在应用层级跟踪订阅状态。自动响应状态变化,无需手动轮询。

基本用法:

@main
struct MyApp: App {
    @State private var customerStatus: CustomerStatus = .unknown

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.customerSubscriptionStatus, customerStatus)
                .subscriptionStatusTask(for: "your.group.id") { statuses in
                    if statuses.contains(where: { $0.state == .subscribed }) {
                        customerStatus = .subscribed
                    } else if statuses.contains(where: { $0.state == .expired }) {
                        customerStatus = .expired
                    } else {
                        customerStatus = .notSubscribed
                    }
                }
        }
    }
}

关键行为:

  • 在应用启动时触发,并提供当前状态
  • 订阅状态变化时再次触发(续订、过期、升级)
  • 将 StoreKit 状态转换为应用模型,让领域模型保持简单
  • 添加到视图层级的顶层(App 或根 WindowGroup)

Offer Codes(iOS 18.2+)

概述

Offer codes 现在支持所有产品类型(此前仅支持订阅):

  • 消耗型
  • 非消耗型
  • 非自动续订订阅
  • 自动续订订阅
在应用中兑换

UIKit:

func showOfferCodeSheet() {
    guard let scene = view.window?.windowScene else { return }

    StoreKit.AppStore.presentOfferCodeRedeemSheet(in: scene)
}
来自 WWDC 2025-241:7:38

SwiftUI:

.offerCodeRedemption(isPresented: $showRedeemSheet)
付款模式

新增:.oneTime:

let transaction: Transaction

if let offer = transaction.offer {
    switch offer.paymentMode {
    case .freeTrial:
        // No charge during offer period
    case .payAsYouGo:
        // Discounted price per billing period
    case .payUpFront:
        // One-time discounted price for entire duration
    case .oneTime:
        // ✨ New: One-time offer code redemption (iOS 17.2+)
    @unknown default:
        break
    }
}
来自 WWDC 2025-241:8:17

旧版访问方式(iOS 15-17.1):

if let offerMode = transaction.offerPaymentModeStringRepresentation {
    // String representation for older OS versions
    print(offerMode) // "oneTime"
}
来自 WWDC 2025-241:8:49

App Store Server Library

概述

用于签署 IAP 请求和解码服务器 API 响应的开源库。支持 Swift、Java、Python、Node.js。

创建促销优惠签名

Swift 示例:

import AppStoreServerLibrary

// Configure signing
let signingKey = "YOUR_PRIVATE_KEY"
let keyID = "YOUR_KEY_ID"
let issuerID = "YOUR_ISSUER_ID"
let bundleID = "com.app.bundle"

let creator = PromotionalOfferV2SignatureCreator(
    privateKey: signingKey,
    keyID: keyID,
    issuerID: issuerID,
    bundleID: bundleID
)

// Create signature
let productID = "com.app.pro_monthly"
let offerID = "promo_winback"
let transactionID = transaction.id // Optional but recommended

let signature = try creator.createSignature(
    productIdentifier: productID,
    subscriptionOfferIdentifier: offerID,
    applicationUsername: nil,
    nonce: UUID(),
    timestamp: Date().timeIntervalSince1970,
    transactionIdentifier: transactionID
)

// Send signature to app
return signature // Compact JWS string
来自 WWDC 2025-241:12:44、2025-249

服务器端点示例:

app.get("promo-offer") { req async throws -> String in
    let productID = try req.query.get(String.self, at: "productID")
    let offerID = try req.query.get(String.self, at: "offerID")

    let signature = try creator.createSignature(
        productIdentifier: productID,
        subscriptionOfferIdentifier: offerID,
        transactionIdentifier: nil
    )

    return signature
}
来自 WWDC 2025-241:12:52

App Store Server API

设置 App Account Token

端点:

PATCH /inApps/v1/transactions/{originalTransactionId}

请求正文:

{
  "appAccountToken": "550e8400-e29b-41d4-a716-446655440000"
}

用法:

  • 为在应用外进行的购买(offer codes、App Store)设置 appAccountToken
  • 账户所有权变更时更新 appAccountToken
  • 将交易与服务器上的客户账户关联
来自 WWDC 2025-249:5:19
获取 App Transaction 信息

端点:

GET /inApps/v2/appTransaction/{transactionId}

响应:

{
  "signedAppTransactionInfo": "eyJhbGc..."
}

用法:

  • 在服务器上获取应用下载信息
  • 检查应用版本、平台和环境
  • 将于 2025 年晚些时候推出
来自 WWDC 2025-249:10:48
发送消费信息 V2

端点:

PUT /inApps/v2/transactions/consumption/{transactionId}

请求正文:

{
  "customerConsented": true,
  "sampleContentProvided": false,
  "deliveryStatus": "DELIVERED",
  "refundPreference": "GRANT_PRORATED",
  "consumptionPercentage": 25000
}

字段:

  • customerConsented(必填):用户同意发送消费数据
  • sampleContentProvided(可选):购买前提供的样例
  • deliveryStatus(必填):"DELIVERED" 或各种 UNDELIVERED 状态
  • refundPreference(可选):"NO_REFUND"、"GRANT_REFUND"、"GRANT_PRORATED"
  • consumptionPercentage(可选):0-100000(千分比,例如 25000 = 25%)

按比例退款:

  • 2025 年新增
  • 支持部分消费(消耗型、非消耗型、非自动续订)
  • 对于自动续订订阅,App Store 会根据剩余时间计算
来自 WWDC 2025-249:16:09
退款通知

REFUND 通知:

{
  "notificationType": "REFUND",
  "data": {
    "signedTransactionInfo": "...",
    "refundPercentage": 75,
    "revocationType": "REFUND_PRORATED"
  }
}

revocationType 值:

  • REFUND_FULL:全额退款 — 撤销所有访问权限
  • REFUND_PRORATED:部分退款 — 按比例撤销访问权限
  • FAMILY_REVOKE:已移除家庭共享 — 撤销访问权限
来自 WWDC 2025-249:20:17

边缘情况

家庭共享

检测家庭共享交易:

// appAccountToken is NOT available for family shared transactions
let transaction: Transaction

if transaction.appAccountToken == nil {
    // Might be family shared (or appAccountToken not set)
    // Check ownershipType (if available)
}

家庭共享的订阅状态:

// Each family member has unique appTransactionID
// Use appTransactionID to identify individual family members
来自 WWDC 2025-241:1:54
退款

处理退款:

func handleTransaction(_ transaction: Transaction) async {
    if let revocationDate = transaction.revocationDate {
        // Transaction was refunded
        print("Refunded on \(revocationDate)")

        switch transaction.revocationReason {
        case .developerIssue:
            // Refund due to app issue
        case .other:
            // Other refund reason
        @unknown default:
            break
        }

        // Revoke entitlement
        await revokeEntitlement(for: transaction.productID)
    }
}
Advanced Commerce API

Advanced Commerce API 支持:

  • 大型内容目录的应用内购买
  • 创作者体验(打赏、赞助)
  • 提供可选附加项的订阅

检查交易是否使用 Advanced Commerce:

if transaction.advancedCommerceInfo != nil {
    // Transaction from Advanced Commerce API
    // Large catalogs, creator experiences, subscriptions with add-ons
}

可通过 Transaction 和 RenewalInfo 上的 advancedCommerceInfo 字段访问。标准 IAP 交易会返回 nil。

来自 WWDC 2025-241:4:51
挽回优惠

为已过期的订阅显示挽回优惠:

let renewalInfo: RenewalInfo

if renewalInfo.expirationReason == .didNotConsentToPriceIncrease {
    // Perfect time for win-back offer!
    SubscriptionOfferView(
        groupID: groupID,
        visibleRelationship: .current
    )
    .preferredSubscriptionOffer(offer: winBackOffer)
}
来自 WWDC 2025-241:5:38

测试

StoreKit 配置文件

创建:

  1. Xcode → 文件 → 新建 → StoreKit 配置文件
  2. 添加产品(消耗型、非消耗型、订阅)
  3. 配置价格、图片和描述

在 Scheme 中启用:

  1. Scheme → 编辑 Scheme → 运行 → 选项
  2. StoreKit 配置:选择 .storekit 文件

测试场景:

  • 成功购买
  • 取消购买
  • 订阅续订(加速时间)
  • 订阅到期
  • 升级/降级
  • 兑换优惠码
  • 家庭共享(在配置文件中启用)
交易管理器

使用 Xcode 中的交易管理器窗口,在测试期间检查和操作交易:

  • 手动创建交易(测试特定的购买流程)
  • 修改交易属性(到期时间、续订状态)
  • 测试订阅优惠场景
  • 检查交易详情和验证状态

打开:调试 → StoreKit → 管理交易(运行时需使用 StoreKit 配置)

沙盒测试

创建沙盒账户:

  1. App Store Connect → 用户与访问权限 → 沙盒测试员
  2. 创建测试 Apple ID
  3. 在设备上登录:设置 → App Store → 沙盒账户

清除购买历史记录:

  • 设置 → App Store → 沙盒账户 → 清除购买历史记录

从 StoreKit 1 迁移

主要变化

代理 → Async/Await:

// StoreKit 1
class StoreObserver: NSObject, SKPaymentTransactionObserver {
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        // Handle transactions
    }
}

// StoreKit 2
for await result in Transaction.updates {
    // Handle transactions
}

收据 → 交易:

// StoreKit 1
let receiptURL = Bundle.main.appStoreReceiptURL
let receipt = try Data(contentsOf: receiptURL!)

// StoreKit 2
let transaction: Transaction // Automatically verified!

产品 → Product.products(for:):

// StoreKit 1
let request = SKProductsRequest(productIdentifiers: Set(productIDs))
request.delegate = self
request.start()

// StoreKit 2
let products = try await Product.products(for: productIDs)

资源

WWDC:2025-241, 2025-249, 2024-10061, 2024-10062, 2024-10110, 2023-10013, 2023-10140, 2022-10007, 2022-110404, 2021-10114

文档:/storekit

技能:axiom-in-app-purchases


快速参考

产品类型
  • .consumable - 可多次购买(金币、增益道具)
  • .nonConsumable - 购买一次,永久拥有(高级版、关卡包)
  • .autoRenewable - 自动续订订阅
  • .nonRenewing - 固定期限订阅
交易状态
  • success - 购买已完成
  • userCancelled - 用户点击了取消
  • pending - 需要执行操作(询问是否购买)
订阅状态
  • .subscribed - 订阅有效
  • .expired - 订阅已结束
  • .inGracePeriod - 存在账单问题,但仍保留访问权限
  • .inBillingRetryPeriod - Apple 正在重试付款
  • .revoked - 家庭共享已移除
必要调用
// Load products
try await Product.products(for: productIDs)

// Purchase
try await product.purchase(confirmIn: scene)

// Current entitlements
Transaction.currentEntitlements(for: productID)

// Transaction listener
Transaction.updates

// Subscription status
Product.SubscriptionInfo.status(for: groupID)

// Restore purchases
try await AppStore.sync()

// Finish transaction (REQUIRED)
await transaction.finish()

评论

支持 Markdown