🛍️ SwiftUI 使用 StoreKit2 实现内购
2023/11/27IAP 类型
- Consumables - 消耗品, 购买一次、多次和批量购买的产品。比如用于游戏充钱提升等级。
- Non-consumables - 非消耗品, 只能购买一次且不会过期的产品。一般用于永久解锁高级功能。👀
- Auto-renewable subscriptions - 自动续期订阅, 用于自动订阅解锁功能,直至用户取消订阅。🚀
- Non-renewing subscriptions - 非续订订阅,到期结束,不自动续订。 比如游戏的季卡等。
📢 IAP 需在 App Connect 创建内购,或订阅及分组。包括内购/订阅id、价格、信息描述(多语言)等。
内购配置:需要在connect上配置订阅项目,另外Xcode - Targets - Signing & Capabilities
上添加 In-App Purchase
StoreKit 配置文件
在 Xcode13 ,创建 .storekit 配置文件集成内购测试,填写配置完成后,设置 Debug .storekit 配置文件。
参考代码
import Foundation
import StoreKit
import SwiftUI
// 购买结果
enum PurchaseResult {
case purchased
case unverified(Error)
case pending
case cancelled
case error
}
@MainActor
final class PurchaseViewModel: ObservableObject {
// MARK: 产品 id
private let productIds = ["pro_monthly", "pro_yearly", "pro_lifetime"]
private var productsLoaded = false
private var updates: Task<Void, Never>? = nil
// MARK: 已购买的 product ids
private(set) var purchasedProductIds = Set<String>()
@Published
private(set) var products: [Product] = []
@Published
private(set) var purchaseResult: PurchaseResult?
@AppStorage("app.global.hasPro")
private(set) var hasPro = false
init() {
self.updates = observeTransactionUpdates()
Task {
await self.getProducts()
}
}
deinit {
self.updates?.cancel()
}
}
extension PurchaseViewModel {
// MARK: 获取 IAP 列表
func getProducts() async {
do {
guard !self.productsLoaded else { return }
// 按价格排序
let products = try await Product.products(for: productIds)
let productsSorted = products.sorted { $0.price < $1.price }
self.productsLoaded = true
self.products = productsSorted
} catch {
self.purchaseResult = .error
}
}
// MARK: 执行购买
func purchase(_ product: Product) async {
do {
let result = try await product.purchase()
switch result {
case let .success(.verified(transaction)):
// 购买并且验证成功
await transaction.finish()
await self.updatePurchasedProducts()
self.purchaseResult = .purchased
case let .success(.unverified(_, error)):
self.purchaseResult = .unverified(error)
case .pending:
self.purchaseResult = .pending
case .userCancelled:
self.purchaseResult = .cancelled
@unknown default:
self.purchaseResult = .error
}
} catch {
self.purchaseResult = .error
}
}
// MARK: 恢复购买
func restorePurchase() async {
try? await AppStore.sync()
}
// MARK: 更新已购买的列表Id
func updatePurchasedProducts() async {
for await result in Transaction.currentEntitlements {
guard case .verified(let transaction) = result else { continue }
if transaction.revocationDate == nil {
self.purchasedProductIds.insert(transaction.productID)
} else {
self.purchasedProductIds.remove(transaction.productID)
}
}
// 判断是否有购买,缓存到 UserDefaults
self.hasPro = !self.purchasedProductIds.isEmpty
}
// MARK: 监听购买 (用于用户已经购买过,执行更新)
func observeTransactionUpdates() -> Task<Void, Never> {
Task(priority: .background) {
for await _ in Transaction.updates {
await self.updatePurchasedProducts()
}
}
}
}
建议在 App 启动时候初始化内购代码,以便优先加载内购产品。
可能存在审核的坑:
1、苹果要求内购产品页要有使用条款的文本或链接,Apple 提供了适用于所有地区的标准最终用户许可协议(EULA),可以在页面引用。https://www.apple.com/legal/internet-services/itunes/dev/stdeula