axiom-storekit-ref by charleswiltgen/axiom
npx skills add https://github.com/charleswiltgen/axiom --skill axiom-storekit-refStoreKit 2 是苹果现代的应用内购买框架,具有 async/await API、自动收据验证和 SwiftUI 集成。本参考涵盖所有 API、iOS 18.4 增强功能以及全面的 WWDC 2025 代码示例。
消耗型 :
非消耗型 :
自动续订订阅 :
非续订订阅 :
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
在以下情况下使用本参考:
相关技能 :
axiom-in-app-purchases — 采用测试优先工作流、架构模式的学科技能iap-auditor 代理)iap-implementation 代理)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)
处理缺失产品 :
let products = try await Product.products(for: productIDs)
// 检查已加载的产品
let loadedIDs = Set(products.map { $0.id })
let missingIDs = Set(productIDs).subtracting(loadedIDs)
if !missingIDs.isEmpty {
print("缺失产品: \(missingIDs)")
// 产品未在 App Store Connect 或 .storekit 文件中配置
}
基本属性 :
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:
// 金币、提示、加速
case .nonConsumable:
// 高级功能、关卡包
case .autoRenewable:
// 月度/年度订阅
case .nonRenewing:
// 季节性通行证
@unknown default:
break
}
检查产品是否为订阅 :
if let subscriptionInfo = product.subscription {
// 产品是自动续订订阅
let groupID = subscriptionInfo.subscriptionGroupID
let period = subscriptionInfo.subscriptionPeriod
}
订阅周期 :
let period = product.subscription?.subscriptionPeriod
switch period?.unit {
case .day:
print("\(period?.value ?? 0) 天")
case .week:
print("\(period?.value ?? 0) 周")
case .month:
print("\(period?.value ?? 0) 月")
case .year:
print("\(period?.value ?? 0) 年")
default:
break
}
介绍性优惠 :
if let introOffer = product.subscription?.introductoryOffer {
print("免费试用: \(introOffer.period.value) \(introOffer.period.unit)")
print("价格: \(introOffer.displayPrice)")
switch introOffer.paymentMode {
case .freeTrial:
print("免费试用 - 不收费")
case .payAsYouGo:
print("每周期折扣价")
case .payUpFront:
print("一次性折扣价")
@unknown default:
break
}
}
促销优惠 :
let offers = product.subscription?.promotionalOffers ?? []
for offer in offers {
print("优惠 ID: \(offer.id)")
print("价格: \(offer.displayPrice)")
print("周期: \(offer.period.value) \(offer.period.unit)")
}
使用 UI 上下文购买 (iOS 18.2+) :
let product: Product
let scene: UIWindowScene
let result = try await product.purchase(confirmIn: scene)
使用选项购买 :
let accountToken = UUID()
let result = try await product.purchase(
confirmIn: scene,
options: [
.appAccountToken(accountToken)
]
)
使用促销优惠购买 (JWS 格式) :
let jwsSignature: String // 来自您的服务器
let result = try await product.purchase(
confirmIn: scene,
options: [
.promotionalOffer(offerID: "promo_winback", signature: jwsSignature)
]
)
使用自定义介绍性优惠资格购买 :
let jwsSignature: String // 来自您的服务器
let result = try await product.purchase(
confirmIn: scene,
options: [
.introductoryOfferEligibility(signature: jwsSignature)
]
)
SwiftUI 购买 (使用环境) :
struct ProductView: View {
let product: Product
@Environment(\.purchase) private var purchase
var body: some View {
Button("购买 \(product.displayPrice)") {
Task {
do {
let result = try await purchase(product)
// 处理结果
} catch {
print("购买失败: \(error)")
}
}
}
}
}
处理购买结果 :
let result = try await product.purchase(confirmIn: scene)
switch result {
case .success(let verificationResult):
// 购买成功 - 验证交易
guard let transaction = try? verificationResult.payloadValue else {
print("交易验证失败")
return
}
// 授予权益
await grantEntitlement(for: transaction)
await transaction.finish()
case .userCancelled:
// 用户在支付页面点击了"取消"
print("用户取消了购买")
case .pending:
// 购买需要操作(询问购买、支付问题)
// 交易批准后将通过 Transaction.updates 到达
print("购买待批准")
@unknown default:
break
}
Transaction 表示一次成功的应用内购买。包含购买元数据、产品 ID、购买日期,对于订阅,还包含到期日期。
appTransactionID :
let transaction: Transaction
let appTransactionID = transaction.appTransactionID
// 应用下载的唯一 ID(同一 Apple 账户的所有购买都相同)
offerPeriod :
if let offerPeriod = transaction.offer?.period {
print("优惠持续时间: \(offerPeriod)")
// ISO 8601 持续时间格式(例如,"P1M" 表示 1 个月)
}
advancedCommerceInfo :
if let advancedInfo = transaction.advancedCommerceInfo {
// 仅适用于高级商务 API 购买
// 标准 IAP 为 nil
}
基本字段 :
let transaction: Transaction
transaction.id // 唯一交易 ID
transaction.originalID // 原始交易 ID(在续订中保持一致)
transaction.productID // "com.app.pro_monthly"
transaction.productType // .autoRenewable
transaction.purchaseDate // 购买日期
transaction.appAccountToken // 购买时设置的 UUID(如果提供)
订阅字段 :
transaction.expirationDate // 订阅到期时间
transaction.isUpgraded // 如果用户升级到更高层级则为 true
transaction.revocationDate // 退款日期(如果未退款则为 nil)
transaction.revocationReason // .developerIssue 或 .other
优惠字段 :
if let offer = transaction.offer {
offer.type // .introductory 或 .promotional 或 .code
offer.id // 来自 App Store Connect 的优惠标识符
offer.paymentMode // .freeTrial, .payAsYouGo, .payUpFront, .oneTime
}
获取所有当前权益 :
var purchasedProductIDs: Set<String> = []
for await result in Transaction.currentEntitlements {
guard let transaction = try? result.payloadValue else {
continue
}
// 仅包含未退款的交易
if transaction.revocationDate == nil {
purchasedProductIDs.insert(transaction.productID)
}
}
获取特定产品的权益 (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 {
// 用户拥有此产品
return true
}
}
已弃用的 API (iOS 18.4) :
// ❌ 在 iOS 18.4 中已弃用
let entitlement = await Transaction.currentEntitlement(for: productID)
// ✅ 改用此方法(返回序列,处理家庭共享)
for await result in Transaction.currentEntitlements(for: productID) {
// ...
}
获取所有交易 :
for await result in Transaction.all {
guard let transaction = try? result.payloadValue else {
continue
}
print("交易: \(transaction.productID) 于 \(transaction.purchaseDate)")
}
获取产品的交易 :
for await result in Transaction.all(matching: productID) {
guard let transaction = try? result.payloadValue else {
continue
}
// 此产品的所有交易
}
监听实时更新(必需) :
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
}
// 授予或撤销权益
if transaction.revocationDate != nil {
await revokeEntitlement(for: transaction.productID)
} else {
await grantEntitlement(for: transaction)
}
// 关键:始终完成交易
await transaction.finish()
}
交易来源 :
VerificationResult :
let result: VerificationResult<Transaction>
switch result {
case .verified(let transaction):
// ✅ 交易由 App Store 签名
await grantEntitlement(for: transaction)
await transaction.finish()
case .unverified(let transaction, let error):
// ❌ 交易签名无效
print("未验证: \(error)")
// 不要授予权益
await transaction.finish() // 仍需完成以清空队列
}
验证检查的内容 :
始终调用 finish() :
await transaction.finish()
何时完成 :
如果不完成会发生什么 :
Transaction.updates 重新发出交易AppTransaction 表示原始应用下载。可通过 AppTransaction.shared 获取。
appTransactionID :
let appTransaction = try await AppTransaction.shared
switch appTransaction {
case .verified(let transaction):
let appTransactionID = transaction.appTransactionID
// 此 Apple 账户 + 应用的全局唯一 ID
// 相同的值出现在 Transaction 和 RenewalInfo 中
case .unverified(_, let error):
print("AppTransaction 验证失败: \(error)")
}
originalPlatform :
if let appTransaction = try? await AppTransaction.shared.payloadValue {
let platform = appTransaction.originalPlatform
switch platform {
case .iOS:
print("最初在 iPhone/iPad 上下载")
case .macOS:
print("最初在 Mac 上下载")
case .tvOS:
print("最初在 Apple TV 上下载")
case .visionOS:
print("最初在 Vision Pro 上下载")
@unknown default:
break
}
}
注意 :在 watchOS 上下载的应用显示 originalPlatform = .iOS
let appTransaction: AppTransaction
appTransaction.appVersion // "1.2.3"
appTransaction.originalAppVersion // "1.0.0"
appTransaction.originalPurchaseDate // 首次下载日期
appTransaction.bundleID // "com.company.app"
appTransaction.deviceVerification // 设备的 UUID
appTransaction.deviceVerificationNonce // 验证的随机数
检查应用版本 :
if let appTransaction = try? await AppTransaction.shared.payloadValue {
if appTransaction.appVersion != currentVersion {
// 提示用户更新
}
}
商业模式迁移 :
// 从付费应用迁移到带 IAP 的免费应用
if appTransaction.originalPlatform == .iOS,
appTransaction.originalPurchaseDate < migrationDate {
// 用户在迁移前已付费购买应用 - 授予高级权限
await grantPremiumAccess()
}
RenewalInfo 提供关于自动续订订阅续订状态的信息,包括是否将续订、到期原因以及即将到来的优惠。
appTransactionID :
let renewalInfo: RenewalInfo
let appTransactionID = renewalInfo.appTransactionID
offerPeriod :
if let offerPeriod = renewalInfo.offerPeriod {
print("下次续订优惠周期: \(offerPeriod)")
// ISO 8601 持续时间(适用于下次续订)
}
appAccountToken :
if let token = renewalInfo.appAccountToken {
// 将订阅与您的服务器账户关联的 UUID
}
advancedCommerceInfo :
if let advancedInfo = renewalInfo.advancedCommerceInfo {
// 仅适用于高级商务 API 订阅
}
续订状态 :
let renewalInfo: RenewalInfo
renewalInfo.willAutoRenew // 如果订阅将续订则为 true
renewalInfo.autoRenewPreference // 客户将续订到的产品 ID
renewalInfo.expirationReason // 订阅到期的原因(如果已到期)
到期原因 :
switch renewalInfo.expirationReason {
case .autoRenewDisabled:
// 用户关闭了自动续订
case .billingError:
// 支付方式问题
case .didNotConsentToPriceIncrease:
// 用户未接受价格上涨 - 显示挽回优惠!
case .productUnavailable:
// 产品不再可用
case .unknown:
// 未知原因
@unknown default:
break
}
宽限期 :
if let gracePeriodExpiration = renewalInfo.gracePeriodExpirationDate {
// 订阅处于宽限期 - 账单问题
// 显示更新支付方式 UI
}
价格上涨同意 :
if let consentStatus = renewalInfo.priceIncreaseStatus {
switch consentStatus {
case .agreed:
// 用户接受了价格上涨
case .notYetResponded:
// 用户尚未响应 - 显示同意 UI
@unknown default:
break
}
}
从 SubscriptionStatus :
let statuses = try await Product.SubscriptionInfo.status(for: groupID)
for status in statuses {
switch status.renewalInfo {
case .verified(let renewalInfo):
print("将续订: \(renewalInfo.willAutoRenew)")
case .unverified(_, let error):
print("续订信息验证失败: \(error)")
}
}
SubscriptionStatus 表示自动续订订阅的当前状态,包括是否活跃、已到期、处于宽限期或处于账单重试期。
状态枚举 :
let status: Product.SubscriptionInfo.Status
switch status.state {
case .subscribed:
// 用户拥有活跃订阅 - 完全访问
case .expired:
// 订阅已到期 - 显示重新订阅/挽回优惠
case .inGracePeriod:
// 账单问题但保持访问权限 - 显示更新支付 UI
case .inBillingRetryPeriod:
// Apple 正在重试支付 - 保持访问权限
case .revoked:
// 家庭共享访问权限被移除 - 撤销访问权限
@unknown default:
break
}
对于订阅组 :
let groupID = "pro_tier"
let statuses = try await Product.SubscriptionInfo.status(for: groupID)
// 查找最高服务级别
let activeStatus = statuses
.filter { $0.state == .subscribed }
.max { $0.transaction.productID < $1.transaction.productID }
对于特定交易 (iOS 18.4+) :
let transactionID = transaction.id
let status = try await Product.SubscriptionInfo.status(for: transactionID)
监听状态更新 :
for await statuses in Product.SubscriptionInfo.Status.updates(for: groupID) {
// 处理更新的状态
for status in statuses {
print("状态: \(status.state)")
}
}
let status: Product.SubscriptionInfo.Status
status.state // .subscribed, .expired 等
status.transaction // VerificationResult<Transaction>
status.renewalInfo // VerificationResult<RenewalInfo>
基本用法 :
import StoreKit
struct ContentView: View {
let productID = "com.app.premium"
var body: some View {
ProductView(id: productID)
}
}
使用已加载的产品 :
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) // 默认
ProductView(id: productID)
.productViewStyle(.compact) // 较小
ProductView(id: productID)
.productViewStyle(.large) // 突出
基本商店 :
struct ContentView: View {
let productIDs = [
"com.app.coins_100",
"com.app.coins_500",
"com.app.coins_1000"
]
var body: some View {
StoreView(ids: productIDs)
}
}
使用已加载的产品 :
struct ContentView: View {
let products: [Product]
var body: some View {
StoreView(products: products)
}
}
基本订阅商店 :
struct SubscriptionView: View {
let groupID = "pro_tier"
var body: some View {
SubscriptionStoreView(groupID: groupID) {
// 订阅选项上方的营销内容
VStack {
Image("app-icon")
Text("升级专业版")
.font(.largeTitle.bold())
Text("解锁所有功能")
}
}
}
}
控制样式 :
SubscriptionStoreView(groupID: groupID) {
// 营销内容
}
.subscriptionStoreControlStyle(.automatic) // 默认
.subscriptionStoreControlStyle(.picker) // 水平选择器
.subscriptionStoreControlStyle(.buttons) // 堆叠按钮
.subscriptionStoreControlStyle(.prominentPicker) // 大型选择器 (iOS 18.4+)
基本优惠视图 :
struct ContentView: View {
let productID = "com.app.pro_monthly"
var body: some View {
SubscriptionOfferView(id: productID)
}
}
使用已加载的产品 :
let product: Product // 已通过 Product.products(for:) 加载
SubscriptionOfferView(product: product)
使用促销图标 :
SubscriptionOfferView(
id: productID,
prefersPromotionalIcon: true
)
// 也可作为修饰符使用
SubscriptionOfferView(id: productID)
.prefersPromotionalIcon(true)
使用自定义图标 :
SubscriptionOfferView(id: productID) {
Image("custom-icon")
.resizable()
.frame(width: 60, height: 60)
} placeholder: {
Image(systemName: "photo")
.foregroundStyle(.gray)
}
使用详情操作 :
@State private var showStore = false
var body: some View {
SubscriptionOfferView(id: productID)
.subscriptionOfferViewDetailAction {
showStore = true
}
.sheet(isPresented: $showStore) {
SubscriptionStoreView(groupID: "pro_tier")
}
}
可见关系 :
// 仅当客户可以升级时显示
SubscriptionOfferView(
groupID: "pro_tier",
visibleRelationship: .upgrade
)
// 仅当客户可以降级时显示
SubscriptionOfferView(
groupID: "pro_tier",
visibleRelationship: .downgrade
)
// 显示交叉升级选项(相同层级,不同计费周期)
SubscriptionOfferView(
groupID: "pro_tier",
visibleRelationship: .crossgrade
)
// 显示当前订阅(仅当有优惠可用时)
SubscriptionOfferView(
groupID: "pro_tier",
visibleRelationship: .current
)
// 显示组中的任何计划
SubscriptionOfferView(
groupID: "pro_tier",
visibleRelationship: .all
)
使用应用图标 :
SubscriptionOfferView(
groupID: groupID,
visibleRelationship: .all,
useAppIcon: true
)
促销优惠 (JWS) :
SubscriptionStoreView(groupID: groupID)
.subscriptionPromotionalOffer(
for: { subscription in
// 返回此订阅的优惠
return subscription.promotionalOffers.first
},
signature: { subscription, offer in
// 从服务器获取 JWS 签名
let signature = try await server.signOffer(
productID: subscription.id,
offerID: offer.id
)
return signature
}
)
使用 SwiftUI 修饰符在应用级别跟踪订阅状态。通过自动响应状态变化来消除手动轮询。
基本用法 :
@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
}
}
}
}
}
关键行为 :
优惠码现在支持所有产品类型(以前仅限订阅):
UIKit :
func showOfferCodeSheet() {
guard let scene = view.window?.windowScene else { return }
StoreKit.AppStore.presentOfferCodeRedeemSheet(in: scene)
}
SwiftUI :
.offerCodeRedemption(isPresented: $showRedeemSheet)
新增:.oneTime :
let transaction: Transaction
if let offer = transaction.offer {
switch offer.paymentMode {
case .freeTrial:
// 优惠期内不收费
case .payAsYouGo:
// 每计费周期折扣价
case .payUpFront:
// 整个持续时间的一次性折扣价
case .oneTime:
// ✨ 新增:一次性优惠码兑换 (iOS 17.2+)
@unknown default:
break
}
}
旧版访问 (iOS 15-17.1) :
if let offerMode = transaction.offerPaymentModeStringRepresentation {
// 旧版操作系统的字符串表示
print(offerMode) // "oneTime"
}
用于签名 IAP 请求和解码服务器 API 响应的开源库。提供 Swift、Java、Python、Node.js 版本。
Swift 示例 :
import AppStoreServerLibrary
// 配置签名
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
)
// 创建签名
let productID = "com.app.pro_monthly"
let offerID = "promo_winback"
let transactionID = transaction.id // 可选但推荐
let signature = try creator.createSignature(
productIdentifier: productID,
subscriptionOfferIdentifier: offerID,
applicationUsername: nil,
nonce: UUID(),
timestamp: Date().timeIntervalSince1970,
transactionIdentifier: transactionID
)
// 将签名发送到应用
return signature // 紧凑型 JWS 字符串
服务器端点示例 :
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
}
端点 :
PATCH /inApps/v1/transactions/{originalTransactionId}
请求体 :
{
"appAccountToken": "550e8400-e29b-41d4-a716-446655440000"
}
用法 :
端点 :
GET /inApps/v2/appTransaction/{transactionId}
响应 :
{
"signedAppTransactionInfo": "eyJhbGc..."
}
用法 :
端点 :
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%)按比例退款 :
REFUND 通知 :
{
"notificationType": "REFUND",
"data": {
"signedTransactionInfo": "...",
"refundPercentage": 75,
"revocationType": "REFUND_PRORATED"
}
}
revocationType 值 :
REFUND_FULL:100% 退款 - 撤销所有访问权限REFUND_PRORATED:部分退款 - 撤销按比例访问权限FAMILY_REVOKE:家庭共享被移除 - 撤销访问权限检测家庭共享交易 :
// 家庭共享交易不可用 appAccountToken
let transaction: Transaction
if transaction.appAccountToken == nil {
// 可能是家庭共享(或未设置 appAccountToken)
// 检查 ownershipType(如果可用)
}
家庭共享的订阅状态 :
// 每个家庭成员都有唯一的 appTransactionID
// 使用 appTransactionID 识别各个家庭成员
StoreKit 2 is Apple's modern in-app purchase framework with async/await APIs, automatic receipt validation, and SwiftUI integration. This reference covers every API, iOS 18.4 enhancements, and comprehensive WWDC 2025 code examples.
Consumable :
Non-Consumable :
Auto-Renewable Subscription :
Non-Renewing Subscription :
Use this reference when:
Related Skills :
axiom-in-app-purchases — Discipline skill with testing-first workflow, architecture patternsiap-auditor agent for auditing existing IAP code)iap-implementation agent for implementing IAP from scratch)Product represents an in-app purchase item configured in App Store Connect or StoreKit configuration file.
Basic Loading :
import StoreKit
let productIDs = [
"com.app.coins_100",
"com.app.premium",
"com.app.pro_monthly"
]
let products = try await Product.products(for: productIDs)
Handling Missing Products :
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
}
Basic Properties :
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
Product Type Enum :
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
}
Check if Product is Subscription :
if let subscriptionInfo = product.subscription {
// Product is auto-renewable subscription
let groupID = subscriptionInfo.subscriptionGroupID
let period = subscriptionInfo.subscriptionPeriod
}
Subscription Period :
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
}
Introductory Offer :
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
}
}
Promotional Offers :
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)")
}
Purchase with UI Context (iOS 18.2+) :
let product: Product
let scene: UIWindowScene
let result = try await product.purchase(confirmIn: scene)
Purchase with Options :
let accountToken = UUID()
let result = try await product.purchase(
confirmIn: scene,
options: [
.appAccountToken(accountToken)
]
)
Purchase with Promotional Offer (JWS Format) :
let jwsSignature: String // From your server
let result = try await product.purchase(
confirmIn: scene,
options: [
.promotionalOffer(offerID: "promo_winback", signature: jwsSignature)
]
)
Purchase with Custom Intro Eligibility :
let jwsSignature: String // From your server
let result = try await product.purchase(
confirmIn: scene,
options: [
.introductoryOfferEligibility(signature: jwsSignature)
]
)
SwiftUI Purchase (Using 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)")
}
}
}
}
}
Handling Purchase Results :
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
}
Transaction represents a successful in-app purchase. Contains purchase metadata, product ID, purchase date, and for subscriptions, expiration date.
appTransactionID :
let transaction: Transaction
let appTransactionID = transaction.appTransactionID
// Unique ID for app download (same across all purchases by same Apple Account)
offerPeriod :
if let offerPeriod = transaction.offer?.period {
print("Offer duration: \(offerPeriod)")
// ISO 8601 duration format (e.g., "P1M" for 1 month)
}
advancedCommerceInfo :
if let advancedInfo = transaction.advancedCommerceInfo {
// Only present for Advanced Commerce API purchases
// nil for standard IAP
}
Basic Fields :
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)
Subscription Fields :
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
Offer Fields :
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
}
Get All Current Entitlements :
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)
}
}
Get Entitlements for Specific Product (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
}
}
Deprecated 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) {
// ...
}
Get All Transactions :
for await result in Transaction.all {
guard let transaction = try? result.payloadValue else {
continue
}
print("Transaction: \(transaction.productID) on \(transaction.purchaseDate)")
}
Get Transactions for Product :
for await result in Transaction.all(matching: productID) {
guard let transaction = try? result.payloadValue else {
continue
}
// All transactions for this product
}
Listen for Real-Time Updates (REQUIRED) :
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()
}
Transaction Sources :
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
}
What Verification Checks :
Always Call finish() :
await transaction.finish()
When to finish :
What happens if you don't finish :
Transaction.updates re-emits transactionAppTransaction represents the original app download. Available via AppTransaction.shared.
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)")
}
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
}
}
Note : Apps downloaded on watchOS show 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
Check App Version :
if let appTransaction = try? await AppTransaction.shared.payloadValue {
if appTransaction.appVersion != currentVersion {
// Prompt user to update
}
}
Business Model Migration :
// 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()
}
RenewalInfo provides information about auto-renewable subscription renewal state, including whether it will renew, expiration reason, and upcoming offers.
appTransactionID :
let renewalInfo: RenewalInfo
let appTransactionID = renewalInfo.appTransactionID
offerPeriod :
if let offerPeriod = renewalInfo.offerPeriod {
print("Next renewal offer period: \(offerPeriod)")
// ISO 8601 duration (applies at next renewal)
}
appAccountToken :
if let token = renewalInfo.appAccountToken {
// UUID associating subscription with your server account
}
advancedCommerceInfo :
if let advancedInfo = renewalInfo.advancedCommerceInfo {
// Only for Advanced Commerce API subscriptions
}
Renewal State :
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)
Expiration Reasons :
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
}
Grace Period :
if let gracePeriodExpiration = renewalInfo.gracePeriodExpirationDate {
// Subscription in grace period - billing issue
// Show update payment method UI
}
Price Increase Consent :
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
}
}
From 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)")
}
}
SubscriptionStatus represents the current state of an auto-renewable subscription, including whether it's active, expired, in grace period, or in billing retry.
State Enum :
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
}
For Subscription Group :
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 }
For Specific Transaction (iOS 18.4+) :
let transactionID = transaction.id
let status = try await Product.SubscriptionInfo.status(for: transactionID)
Listen for Status Updates :
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>
Basic Usage :
import StoreKit
struct ContentView: View {
let productID = "com.app.premium"
var body: some View {
ProductView(id: productID)
}
}
With Loaded Product :
struct ContentView: View {
let product: Product
var body: some View {
ProductView(for: product)
}
}
Custom Icon :
ProductView(id: productID) {
Image(systemName: "star.fill")
.foregroundStyle(.yellow)
}
Control Styles :
ProductView(id: productID)
.productViewStyle(.regular) // Default
ProductView(id: productID)
.productViewStyle(.compact) // Smaller
ProductView(id: productID)
.productViewStyle(.large) // Prominent
Basic Store :
struct ContentView: View {
let productIDs = [
"com.app.coins_100",
"com.app.coins_500",
"com.app.coins_1000"
]
var body: some View {
StoreView(ids: productIDs)
}
}
With Loaded Products :
struct ContentView: View {
let products: [Product]
var body: some View {
StoreView(products: products)
}
}
Basic Subscription Store :
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")
}
}
}
}
Control Style :
SubscriptionStoreView(groupID: groupID) {
// Marketing content
}
.subscriptionStoreControlStyle(.automatic) // Default
.subscriptionStoreControlStyle(.picker) // Horizontal picker
.subscriptionStoreControlStyle(.buttons) // Stacked buttons
.subscriptionStoreControlStyle(.prominentPicker) // Large picker (iOS 18.4+)
Basic Offer View :
struct ContentView: View {
let productID = "com.app.pro_monthly"
var body: some View {
SubscriptionOfferView(id: productID)
}
}
With Loaded Product :
let product: Product // Already loaded via Product.products(for:)
SubscriptionOfferView(product: product)
With Promotional Icon :
SubscriptionOfferView(
id: productID,
prefersPromotionalIcon: true
)
// Also available as modifier
SubscriptionOfferView(id: productID)
.prefersPromotionalIcon(true)
With Custom Icon :
SubscriptionOfferView(id: productID) {
Image("custom-icon")
.resizable()
.frame(width: 60, height: 60)
} placeholder: {
Image(systemName: "photo")
.foregroundStyle(.gray)
}
With Detail Action :
@State private var showStore = false
var body: some View {
SubscriptionOfferView(id: productID)
.subscriptionOfferViewDetailAction {
showStore = true
}
.sheet(isPresented: $showStore) {
SubscriptionStoreView(groupID: "pro_tier")
}
}
Visible Relationship :
// 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
)
With App Icon :
SubscriptionOfferView(
groupID: groupID,
visibleRelationship: .all,
useAppIcon: true
)
Promotional Offer (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
}
)
Track subscription status at the app level with a SwiftUI modifier. Eliminates manual polling by reacting to status changes automatically.
Basic Usage :
@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
}
}
}
}
}
Key behavior :
Offer codes now support all product types (previously subscription-only):
UIKit :
func showOfferCodeSheet() {
guard let scene = view.window?.windowScene else { return }
StoreKit.AppStore.presentOfferCodeRedeemSheet(in: scene)
}
SwiftUI :
.offerCodeRedemption(isPresented: $showRedeemSheet)
New: .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
}
}
Legacy Access (iOS 15-17.1) :
if let offerMode = transaction.offerPaymentModeStringRepresentation {
// String representation for older OS versions
print(offerMode) // "oneTime"
}
Open-source library for signing IAP requests and decoding server API responses. Available in Swift, Java, Python, Node.js.
Swift Example :
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
Server Endpoint Example :
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
}
Endpoint :
PATCH /inApps/v1/transactions/{originalTransactionId}
Request Body :
{
"appAccountToken": "550e8400-e29b-41d4-a716-446655440000"
}
Usage :
Endpoint :
GET /inApps/v2/appTransaction/{transactionId}
Response :
{
"signedAppTransactionInfo": "eyJhbGc..."
}
Usage :
Endpoint :
PUT /inApps/v2/transactions/consumption/{transactionId}
Request Body :
{
"customerConsented": true,
"sampleContentProvided": false,
"deliveryStatus": "DELIVERED",
"refundPreference": "GRANT_PRORATED",
"consumptionPercentage": 25000
}
Fields :
customerConsented (required): User consented to send consumption datasampleContentProvided (optional): Sample provided before purchasedeliveryStatus (required): "DELIVERED" or various UNDELIVERED statusesrefundPreference (optional): "NO_REFUND", "GRANT_REFUND", "GRANT_PRORATED"consumptionPercentage (optional): 0-100000 (millipercent, e.g., 25000 = 25%)Prorated Refund :
REFUND Notification :
{
"notificationType": "REFUND",
"data": {
"signedTransactionInfo": "...",
"refundPercentage": 75,
"revocationType": "REFUND_PRORATED"
}
}
revocationType Values :
REFUND_FULL: 100% refund - revoke all accessREFUND_PRORATED: Partial refund - revoke proportional accessFAMILY_REVOKE: Family Sharing removed - revoke accessDetect Family Shared Transactions :
// 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)
}
Subscription Status for Family Sharing :
// Each family member has unique appTransactionID
// Use appTransactionID to identify individual family members
Handle Refund :
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)
}
}
The Advanced Commerce API enables support for:
Check if Transaction Uses Advanced Commerce :
if transaction.advancedCommerceInfo != nil {
// Transaction from Advanced Commerce API
// Large catalogs, creator experiences, subscriptions with add-ons
}
Accessible through the advancedCommerceInfo field on both Transaction and RenewalInfo. Returns nil for standard IAP transactions.
Show Win-Back for Expired Subscription :
let renewalInfo: RenewalInfo
if renewalInfo.expirationReason == .didNotConsentToPriceIncrease {
// Perfect time for win-back offer!
SubscriptionOfferView(
groupID: groupID,
visibleRelationship: .current
)
.preferredSubscriptionOffer(offer: winBackOffer)
}
Create :
Enable in Scheme :
Test Scenarios :
Use the Transaction Manager window in Xcode to inspect and manipulate transactions during testing:
Open : Debug → StoreKit → Manage Transactions (while running with StoreKit configuration)
Create Sandbox Account :
Clear Purchase History :
Delegates → 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
}
Receipt → Transaction :
// StoreKit 1
let receiptURL = Bundle.main.appStoreReceiptURL
let receipt = try Data(contentsOf: receiptURL!)
// StoreKit 2
let transaction: Transaction // Automatically verified!
Products → 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
Docs : /storekit
Skills : axiom-in-app-purchases
.consumable - Can purchase multiple times (coins, boosts).nonConsumable - Purchase once, own forever (premium, level packs).autoRenewable - Auto-renewing subscriptions.nonRenewing - Fixed duration subscriptionssuccess - Purchase completeduserCancelled - User tapped cancelpending - Requires action (Ask to Buy).subscribed - Active subscription.expired - Subscription ended.inGracePeriod - Billing issue, access maintained.inBillingRetryPeriod - Apple retrying payment.revoked - Family Sharing removed// 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()
Weekly Installs
109
Repository
GitHub Stars
610
First Seen
Jan 21, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykWarn
Installed on
opencode91
codex84
claude-code83
gemini-cli82
cursor78
github-copilot76
Lark Skill Maker 教程:基于飞书CLI创建AI技能,自动化工作流与API调用指南
31,500 周安装