1. 苹果后台创建(App Store Connect)
-
开发者需在 App Store Connect 后台手动创建商品
-
路径:
功能 → 应用内购买 → 创建(+) -
每个商品需填写:
✅ 商品 ID:唯一标识符(如com.czchat.CZChat01)
✅ 类型:消耗型/非消耗型/订阅
✅ 定价:设置基准价格
✅ 本地化信息:名称和描述(多语言)
2. App 中的使用方式
获取商品信息前,商品 ID 必须内置到 App 代码中(如示例代码所示),常见实现方式:
// 方式1:硬编码(不推荐)
NSArray *productIDs = @[@"com.czchat.CZChat01", @"com.czchat.CZChat02"];
// 方式2:从 Plist 文件读取(推荐)
NSString *path = [[NSBundle mainBundle] pathForResource:@"IAPProducts" ofType:@"plist"];
NSArray *productIDs = [NSArray arrayWithContentsOfFile:path];
// 方式3:从服务器动态获取(最佳实践)
[APIClient fetchProductIDs:^(NSArray *ids) {
NSSet *productSet = [NSSet setWithArray:ids];
SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:productSet];
[request start];
}];
3. 交易处理流程
交易处理流程
4. 模拟内购和订阅处理逻辑
以下是用 Swift 5 和 StoreKit 2 实现的最新内购和订阅逻辑代码,采用现代 Swift 并发编程模式:
import SwiftUI
import StoreKit
// MARK: - 商品模型
struct StoreProduct: Identifiable {
let id: String
let displayName: String
let description: String
let price: String
let skProduct: Product
}
// MARK: - 内购管理器
@MainActor
class StoreManager: ObservableObject {
// 商品集合
@Published var products: [StoreProduct] = []
@Published var subscriptions: [StoreProduct] = []
// 购买状态
@Published var purchaseState: PurchaseState = .idle
@Published var currentEntitlements: [Product] = []
// 错误处理
@Published var errorMessage: String?
// 状态枚举
enum PurchaseState {
case idle
case inProgress
case success(Product)
case restored
case failed
}
// 初始化
init() {
Task {
// 设置交易监听
_ = await Transaction.updates
.map { await self.handle(transaction: $0) }
// 加载商品
await loadProducts()
// 检查当前订阅状态
await checkCurrentEntitlements()
}
}
// 加载所有商品
func loadProducts() async {
do {
// 从服务器获取商品ID列表(实际项目中)
let productIDs = try await fetchProductIDsFromServer()
// 请求商品信息
let storeProducts = try await Product.products(for: productIDs)
// 分类处理
self.products = storeProducts
.filter { $0.type == .consumable || $0.type == .nonConsumable }
.map { StoreProduct(
id: $0.id,
displayName: $0.displayName,
description: $0.description,
price: $0.displayPrice,
skProduct: $0
)}
self.subscriptions = storeProducts
.filter { $0.type == .autoRenewable }
.map { StoreProduct(
id: $0.id,
displayName: $0.displayName,
description: $0.description,
price: $0.displayPrice,
skProduct: $0
)}
} catch {
errorMessage = "加载商品失败: \(error.localizedDescription)"
}
}
// 购买商品
func purchase(_ product: Product) async {
purchaseState = .inProgress
do {
let result = try await product.purchase()
switch result {
case .success(let verification):
// 验证交易结果
if case .verified(let transaction) = verification {
// 通知服务器验证交易
await verifyTransactionWithServer(transaction)
// 更新本地状态
await transaction.finish()
purchaseState = .success(product)
// 更新权益
await checkCurrentEntitlements()
} else {
purchaseState = .failed
errorMessage = "交易验证失败"
}
case .pending:
errorMessage = "交易等待处理"
purchaseState = .idle
case .userCancelled:
errorMessage = "用户取消交易"
purchaseState = .idle
@unknown default:
purchaseState = .idle
}
} catch {
errorMessage = "购买失败: \(error.localizedDescription)"
purchaseState = .failed
}
}
// 恢复购买
func restorePurchases() async {
do {
try await AppStore.sync()
await checkCurrentEntitlements()
purchaseState = .restored
} catch {
errorMessage = "恢复购买失败: \(error.localizedDescription)"
}
}
// 检查当前权益
func checkCurrentEntitlements() async {
var entitlements: [Product] = []
// 检查所有交易
for await result in Transaction.currentEntitlements {
if case .verified(let transaction) = result {
// 获取关联的商品
if let product = products.first(where: { $0.id == transaction.productID })?.skProduct {
entitlements.append(product)
} else if let subscription = subscriptions.first(where: { $0.id == transaction.productID })?.skProduct {
entitlements.append(subscription)
}
}
}
self.currentEntitlements = entitlements
}
// MARK: - 服务器交互
// 从服务器获取商品ID(示例)
private func fetchProductIDsFromServer() async throws -> [String] {
// 实际项目中替换为网络请求
return [
"com.yourcompany.consumable_item",
"com.yourcompany.premium_upgrade",
"com.yourcompany.monthly_subscription"
]
}
// 向服务器验证交易
private func verifyTransactionWithServer(_ transaction: Transaction) async {
guard let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
FileManager.default.fileExists(atPath: appStoreReceiptURL.path) else {
errorMessage = "收据文件不存在"
return
}
do {
let receiptData = try Data(contentsOf: appStoreReceiptURL)
let base64Receipt = receiptData.base64EncodedString()
// 发送到服务器验证
let verificationResult = try await sendReceiptToServer(
receiptData: base64Receipt,
productID: transaction.productID,
transactionID: String(transaction.id)
if verificationResult.success {
print("服务器验证成功")
} else {
errorMessage = "服务器验证失败: \(verificationResult.message ?? "")"
}
} catch {
errorMessage = "收据处理错误: \(error.localizedDescription)"
}
}
// 发送收据到服务器(示例)
private func sendReceiptToServer(receiptData: String, productID: String, transactionID: String) async throws -> VerificationResponse {
// 实际项目中替换为真实网络请求
struct VerificationResponse: Decodable {
let success: Bool
let message: String?
}
// 模拟网络请求
await Task.sleep(1_000_000_000) // 1秒延迟
// 模拟成功响应
return VerificationResponse(success: true, message: "验证成功")
}
// 处理交易更新
private func handle(transaction: VerificationResult<Transaction>) async {
if case .verified(let verifiedTransaction) = transaction {
// 解锁内容
await checkCurrentEntitlements()
// 完成交易
await verifiedTransaction.finish()
}
}
}
// MARK: - 用户界面
struct StoreView: View {
@StateObject private var storeManager = StoreManager()
var body: some View {
NavigationView {
List {
// 消耗型/非消耗型商品
Section(header: Text("商品")) {
ForEach(storeManager.products) { product in
productRow(for: product)
}
}
// 订阅商品
Section(header: Text("订阅")) {
ForEach(storeManager.subscriptions) { product in
productRow(for: product)
}
}
// 恢复购买按钮
Section {
Button("恢复购买") {
Task { await storeManager.restorePurchases() }
}
.frame(maxWidth: .infinity, alignment: .center)
}
// 当前权益
if !storeManager.currentEntitlements.isEmpty {
Section(header: Text("当前权益")) {
ForEach(storeManager.currentEntitlements, id: \.id) { product in
Text(product.displayName)
}
}
}
}
.navigationTitle("应用内购买")
.alert(isPresented: .constant(storeManager.errorMessage != nil)) {
Alert(
title: Text("错误"),
message: Text(storeManager.errorMessage ?? "未知错误"),
dismissButton: .default(Text("确定")) {
storeManager.errorMessage = nil
}
)
}
.overlay {
if case .inProgress = storeManager.purchaseState {
ProgressView("处理中...")
.padding()
.background(Color.secondary.opacity(0.8))
.cornerRadius(10)
}
}
}
}
// 商品行视图
private func productRow(for product: StoreProduct) -> some View {
HStack {
VStack(alignment: .leading) {
Text(product.displayName)
.font(.headline)
Text(product.description)
.font(.subheadline)
.foregroundColor(.secondary)
}
Spacer()
Button(action: {
Task { await storeManager.purchase(product.skProduct) }
}) {
Text(product.price)
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
}
.padding(.vertical, 8)
}
}
// MARK: - 预览
struct StoreView_Previews: PreviewProvider {
static var previews: some View {
StoreView()
}
}
-
使用 StoreKit 2 全新 API
- 采用 Swift 并发 (
async/await) - 使用
Product和Transaction对象模型 - 通过
Transaction.currentEntitlements管理权益
- 采用 Swift 并发 (
-
商品管理优化
- 动态从服务器获取商品ID
- 自动分类消耗型/非消耗型/订阅商品
- 实时更新当前权益状态
-
订阅管理改进
- 自动续期订阅状态跟踪
- 通过
Transaction.updates监听实时变化 - 使用
AppStore.sync()恢复购买
-
错误处理
- 集中式错误处理
- 状态机管理 (
PurchaseState) - 用户友好的错误提示
-
SwiftUI 集成
- 响应式数据绑定 (
@Published) - 主线程安全 (
@MainActor) - 现代化 UI 组件
- 响应式数据绑定 (
最佳实践建议:
-
服务器验证强化
private func verifyTransactionWithServer(_ transaction: Transaction) async { // 获取收据 guard let receiptURL = Bundle.main.appStoreReceiptURL, let receiptData = try? Data(contentsOf: receiptURL) else { return } // 准备验证数据 let requestData: [String: Any] = [ "receipt": receiptData.base64EncodedString(), "product_id": transaction.productID, "transaction_id": String(transaction.id), "environment": transaction.environment.rawValue ] // 发送验证请求 do { let result = try await serverAPI.verifyReceipt(requestData) if result.isValid { // 发放权益 } else { // 处理无效交易 } } catch { // 处理网络错误 } } -
订阅状态管理
func subscriptionStatus(for productID: String) async -> Product.SubscriptionInfo.Status? { guard let product = subscriptions.first(where: { $0.id == productID })?.skProduct, let status = try? await product.subscription?.status else { return nil } // 返回最高优先级状态 return status.max(by: { $0.state < $1.state }) } -
沙盒测试处理
private func verificationURL(for environment: Environment) -> URL { switch environment { case .production: return URL(string: "https://buy.itunes.apple.com/verifyReceipt")! case .sandbox, .xcode: return URL(string: "https://sandbox.itunes.apple.com/verifyReceipt")! @unknown default: return URL(string: "https://sandbox.itunes.apple.com/verifyReceipt")! } } -
促销优惠处理
func checkForPromotionalOffer(for product: Product) async { guard let subscription = product.subscription, let offers = await subscription.promotionalOffers else { return } // 显示可用的促销优惠 for offer in offers { print("促销优惠: \(offer.id) - \(offer.type)") } }
这个实现完全采用 Swift 现代编程范式:
- 使用 SwiftUI 构建声明式 UI
- 采用 Swift 并发处理异步操作
- 利用 StoreKit 2 的新特性
- 符合苹果最新审核指南要求
- 包含完整的错误处理和状态管理
实际部署时,需要:
- 在 App Store Connect 配置商品
- 实现真实的服务器验证端点
- 添加适当的本地化内容
- 配置沙盒测试环境
- 处理订阅续期和生命周期事件
5. 兼容 iOS 14 及以上版本
结合了 StoreKit 1 和 StoreKit 2 的最佳实践:
import SwiftUI
import StoreKit
// MARK: - 商品模型
struct StoreProduct: Identifiable {
let id: String
let displayName: String
let description: String
let price: String
let skProduct: SKProduct?
@available(iOS 15.0, *)
var product: Product? {
get { nil }
set { }
}
}
// MARK: - 内购管理器
class StoreManager: NSObject, ObservableObject, SKProductsRequestDelegate, SKPaymentTransactionObserver {
// 商品集合
@Published var products: [StoreProduct] = []
@Published var subscriptions: [StoreProduct] = []
// 购买状态
@Published var purchaseState: PurchaseState = .idle
@Published var currentEntitlements: [String] = []
// 错误处理
@Published var errorMessage: String?
// 状态枚举
enum PurchaseState {
case idle
case inProgress
case success(String)
case restored
case failed
}
// StoreKit 1 变量
private var productRequest: SKProductsRequest?
private var pendingProduct: SKProduct?
// 初始化
override init() {
super.init()
// 添加交易观察者
SKPaymentQueue.default().add(self)
// 加载商品
loadProducts()
}
deinit {
SKPaymentQueue.default().remove(self)
}
// MARK: - 公共方法
// 加载所有商品
func loadProducts() {
// 从服务器获取商品ID列表(实际项目中)
let productIDs = [
"com.yourcompany.consumable_item",
"com.yourcompany.premium_upgrade",
"com.yourcompany.monthly_subscription"
]
if #available(iOS 15.0, *) {
Task {
await loadProductsWithStoreKit2(for: productIDs)
}
} else {
loadProductsWithStoreKit1(for: productIDs)
}
}
// 购买商品
func purchase(_ product: StoreProduct) {
purchaseState = .inProgress
if #available(iOS 15.0, *) {
Task {
await purchaseWithStoreKit2(product)
}
} else {
purchaseWithStoreKit1(product)
}
}
// 恢复购买
func restorePurchases() {
if #available(iOS 15.0, *) {
Task {
await restoreWithStoreKit2()
}
} else {
restoreWithStoreKit1()
}
}
// MARK: - StoreKit 2 方法 (iOS 15+)
@available(iOS 15.0, *)
private func loadProductsWithStoreKit2(for productIDs: [String]) async {
do {
let storeProducts = try await Product.products(for: productIDs)
await MainActor.run {
// 分类处理
self.products = storeProducts
.filter { $0.type == .consumable || $0.type == .nonConsumable }
.map { StoreProduct(
id: $0.id,
displayName: $0.displayName,
description: $0.description,
price: $0.displayPrice,
skProduct: nil
)}
self.subscriptions = storeProducts
.filter { $0.type == .autoRenewable }
.map { StoreProduct(
id: $0.id,
displayName: $0.displayName,
description: $0.description,
price: $0.displayPrice,
skProduct: nil
)}
}
// 检查当前权益
await checkCurrentEntitlements()
} catch {
await MainActor.run {
errorMessage = "加载商品失败: \(error.localizedDescription)"
}
}
}
@available(iOS 15.0, *)
private func purchaseWithStoreKit2(_ product: StoreProduct) async {
guard let sk2Product = try? await Product.products(for: [product.id]).first else {
await MainActor.run {
errorMessage = "商品不可用"
purchaseState = .failed
}
return
}
do {
let result = try await sk2Product.purchase()
switch result {
case .success(let verification):
// 验证交易结果
if case .verified(let transaction) = verification {
// 通知服务器验证交易
await verifyTransactionWithServer(
productID: transaction.productID,
transactionID: String(transaction.id)
)
// 更新本地状态
await transaction.finish()
await MainActor.run {
purchaseState = .success(product.displayName)
}
// 更新权益
await checkCurrentEntitlements()
} else {
await MainActor.run {
purchaseState = .failed
errorMessage = "交易验证失败"
}
}
case .pending:
await MainActor.run {
errorMessage = "交易等待处理"
purchaseState = .idle
}
case .userCancelled:
await MainActor.run {
errorMessage = "用户取消交易"
purchaseState = .idle
}
@unknown default:
await MainActor.run {
purchaseState = .idle
}
}
} catch {
await MainActor.run {
errorMessage = "购买失败: \(error.localizedDescription)"
purchaseState = .failed
}
}
}
@available(iOS 15.0, *)
private func restoreWithStoreKit2() async {
do {
try await AppStore.sync()
await checkCurrentEntitlements()
await MainActor.run {
purchaseState = .restored
}
} catch {
await MainActor.run {
errorMessage = "恢复购买失败: \(error.localizedDescription)"
}
}
}
@available(iOS 15.0, *)
private func checkCurrentEntitlements() async {
var entitlements: [String] = []
// 检查所有交易
for await result in Transaction.currentEntitlements {
if case .verified(let transaction) = result {
entitlements.append(transaction.productID)
}
}
await MainActor.run {
self.currentEntitlements = entitlements
}
}
// MARK: - StoreKit 1 方法 (iOS 14 兼容)
private func loadProductsWithStoreKit1(for productIDs: [String]) {
let request = SKProductsRequest(productIdentifiers: Set(productIDs))
request.delegate = self
productRequest = request
request.start()
}
private func purchaseWithStoreKit1(_ product: StoreProduct) {
guard let skProduct = product.skProduct else {
errorMessage = "商品信息不完整"
purchaseState = .failed
return
}
let payment = SKPayment(product: skProduct)
SKPaymentQueue.default().add(payment)
}
private func restoreWithStoreKit1() {
SKPaymentQueue.default().restoreCompletedTransactions()
}
// MARK: - SKProductsRequestDelegate (StoreKit 1)
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
// 处理无效商品
if !response.invalidProductIdentifiers.isEmpty {
print("无效商品ID: \(response.invalidProductIdentifiers)")
}
// 处理有效商品
var allProducts: [StoreProduct] = []
var subscriptionProducts: [StoreProduct] = []
for product in response.products {
let storeProduct = StoreProduct(
id: product.productIdentifier,
displayName: product.localizedTitle,
description: product.localizedDescription,
price: formattedPrice(for: product),
skProduct: product
)
// 判断是否为订阅
if product.subscriptionPeriod != nil {
subscriptionProducts.append(storeProduct)
} else {
allProducts.append(storeProduct)
}
}
DispatchQueue.main.async {
self.products = allProducts
self.subscriptions = subscriptionProducts
}
}
func request(_ request: SKRequest, didFailWithError error: Error) {
DispatchQueue.main.async {
self.errorMessage = "商品请求失败: \(error.localizedDescription)"
}
}
// MARK: - SKPaymentTransactionObserver (StoreKit 1)
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchasing:
// 交易正在进行中
break
case .purchased, .restored:
// 交易完成或恢复
handleCompletedTransaction(transaction)
queue.finishTransaction(transaction)
case .failed:
// 交易失败
handleFailedTransaction(transaction)
queue.finishTransaction(transaction)
case .deferred:
// 交易需要家长批准
DispatchQueue.main.async {
self.purchaseState = .idle
self.errorMessage = "购买需要家长批准"
}
@unknown default:
break
}
}
}
func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
DispatchQueue.main.async {
self.purchaseState = .restored
}
}
func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) {
DispatchQueue.main.async {
self.errorMessage = "恢复购买失败: \(error.localizedDescription)"
}
}
// MARK: - 交易处理
private func handleCompletedTransaction(_ transaction: SKPaymentTransaction) {
let productID = transaction.payment.productIdentifier
// 验证收据
verifyReceiptForTransaction(transaction) { [weak self] success in
guard let self = self else { return }
DispatchQueue.main.async {
if success {
self.purchaseState = .success(transaction.payment.productIdentifier)
self.currentEntitlements.append(productID)
} else {
self.errorMessage = "交易验证失败"
self.purchaseState = .failed
}
}
}
}
private func handleFailedTransaction(_ transaction: SKPaymentTransaction) {
DispatchQueue.main.async {
if let error = transaction.error as? SKError {
switch error.code {
case .paymentCancelled:
self.errorMessage = "用户取消交易"
case .paymentNotAllowed:
self.errorMessage = "设备不允许支付"
case .paymentInvalid:
self.errorMessage = "支付参数无效"
case .clientInvalid:
self.errorMessage = "客户端未授权"
default:
self.errorMessage = "交易失败: \(error.localizedDescription)"
}
} else {
self.errorMessage = transaction.error?.localizedDescription ?? "未知错误"
}
self.purchaseState = .failed
}
}
// MARK: - 收据验证
private func verifyReceiptForTransaction(_ transaction: SKPaymentTransaction, completion: @escaping (Bool) -> Void) {
// 获取收据数据
guard let receiptURL = Bundle.main.appStoreReceiptURL,
let receiptData = try? Data(contentsOf: receiptURL) else {
completion(false)
return
}
let base64Receipt = receiptData.base64EncodedString()
// 确定验证环境
let isSandbox = isSandboxReceipt(receiptData)
let verifyURL = isSandbox ?
"https://sandbox.itunes.apple.com/verifyReceipt" :
"https://buy.itunes.apple.com/verifyReceipt"
// 准备请求数据
let requestData: [String: Any] = [
"receipt-data": base64Receipt,
"password": "你的共享密钥", // 仅用于订阅
"exclude-old-transactions": true
]
// 发送验证请求
sendReceiptVerificationRequest(to: verifyURL, data: requestData) { result in
switch result {
case .success(let response):
// 验证响应状态
guard let status = response["status"] as? Int, status == 0 else {
completion(false)
return
}
// 检查是否包含当前交易
if let receiptInfo = response["receipt"] as? [String: Any],
let inApp = receiptInfo["in_app"] as? [[String: Any]] {
let transactionIDs = inApp.compactMap { $0["transaction_id"] as? String }
let originalIDs = inApp.compactMap { $0["original_transaction_id"] as? String }
let allIDs = transactionIDs + originalIDs
let transactionID = String(transaction.transactionIdentifier ?? "")
completion(allIDs.contains(transactionID))
} else {
completion(false)
}
case .failure:
completion(false)
}
}
}
private func verifyTransactionWithServer(productID: String, transactionID: String) async {
// 获取收据数据
guard let receiptURL = Bundle.main.appStoreReceiptURL,
let receiptData = try? Data(contentsOf: receiptURL) else {
await MainActor.run {
errorMessage = "收据文件不存在"
}
return
}
let base64Receipt = receiptData.base64EncodedString()
// 确定验证环境
let isSandbox = isSandboxReceipt(receiptData)
// 发送到服务器验证
do {
let success = try await sendReceiptToServer(
receiptData: base64Receipt,
productID: productID,
transactionID: transactionID,
isSandbox: isSandbox
)
if !success {
await MainActor.run {
errorMessage = "服务器验证失败"
}
}
} catch {
await MainActor.run {
errorMessage = "验证错误: \(error.localizedDescription)"
}
}
}
private func isSandboxReceipt(_ receiptData: Data) -> Bool {
let receiptString = String(data: receiptData, encoding: .ascii) ?? ""
return receiptString.contains("Sandbox") || receiptString.contains("Environment=Sandbox")
}
// MARK: - 服务器交互
private func sendReceiptVerificationRequest(to urlString: String, data: [String: Any], completion: @escaping (Result<[String: Any], Error>) -> Void) {
guard let url = URL(string: urlString),
let jsonData = try? JSONSerialization.data(withJSONObject: data) else {
completion(.failure(NSError(domain: "InvalidRequest", code: 0)))
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = jsonData
request.timeoutInterval = 20
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
completion(.failure(error))
return
}
guard let data = data,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
completion(.failure(NSError(domain: "InvalidResponse", code: 0)))
return
}
completion(.success(json))
}.resume()
}
private func sendReceiptToServer(receiptData: String, productID: String, transactionID: String, isSandbox: Bool) async throws -> Bool {
// 实际项目中替换为真实网络请求
// 这里使用延迟模拟网络请求
try await Task.sleep(nanoseconds: 1_000_000_000) // 1秒延迟
// 模拟成功响应
return true
}
// MARK: - 辅助方法
private func formattedPrice(for product: SKProduct) -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.locale = product.priceLocale
return formatter.string(from: product.price) ?? "\(product.price)"
}
}
// MARK: - 用户界面
struct StoreView: View {
@StateObject private var storeManager = StoreManager()
var body: some View {
NavigationView {
List {
// 消耗型/非消耗型商品
Section(header: Text("商品")) {
ForEach(storeManager.products) { product in
productRow(for: product)
}
}
// 订阅商品
if !storeManager.subscriptions.isEmpty {
Section(header: Text("订阅")) {
ForEach(storeManager.subscriptions) { product in
productRow(for: product)
}
}
}
// 恢复购买按钮
Section {
Button("恢复购买") {
storeManager.restorePurchases()
}
.frame(maxWidth: .infinity, alignment: .center)
}
// 当前权益
if !storeManager.currentEntitlements.isEmpty {
Section(header: Text("当前权益")) {
ForEach(storeManager.currentEntitlements, id: \.self) { productID in
Text(productID)
}
}
}
}
.navigationTitle("应用内购买")
.alert(isPresented: .constant(storeManager.errorMessage != nil)) {
Alert(
title: Text("错误"),
message: Text(storeManager.errorMessage ?? "未知错误"),
dismissButton: .default(Text("确定")) {
storeManager.errorMessage = nil
}
)
}
.overlay {
if case .inProgress = storeManager.purchaseState {
ProgressView("处理中...")
.padding()
.background(Color.secondary.opacity(0.8))
.cornerRadius(10)
}
}
}
}
// 商品行视图
private func productRow(for product: StoreProduct) -> some View {
HStack {
VStack(alignment: .leading) {
Text(product.displayName)
.font(.headline)
Text(product.description)
.font(.subheadline)
.foregroundColor(.secondary)
}
Spacer()
Button(action: {
storeManager.purchase(product)
}) {
Text(product.price)
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
}
.padding(.vertical, 8)
}
}
// MARK: - 预览
struct StoreView_Previews: PreviewProvider {
static var previews: some View {
StoreView()
}
}
- StoreKit 1 实现细节
-
商品加载:使用
SKProductsRequestDelegate获取商品信息 -
交易处理:通过
SKPaymentTransactionObserver监听交易状态 - 收据验证:使用传统方法获取并验证收据
-
恢复购买:调用
restoreCompletedTransactions()
- StoreKit 2 实现细节 (iOS 15+)
-
商品加载:使用
Product.products(for:)异步方法 -
交易处理:通过
Transaction.updates监听交易状态 - 收据验证:自动管理,简化流程
-
恢复购买:使用
AppStore.sync()方法
- 统一接口设计
// 公共方法保持统一
func loadProducts()
func purchase(_ product: StoreProduct)
func restorePurchases()
- 收据验证兼容方案
private func verifyReceiptForTransaction(_ transaction: SKPaymentTransaction, completion: @escaping (Bool) -> Void) {
// 获取收据数据
guard let receiptURL = Bundle.main.appStoreReceiptURL,
let receiptData = try? Data(contentsOf: receiptURL) else {
completion(false)
return
}
// 确定环境(沙盒或生产)
let isSandbox = isSandboxReceipt(receiptData)
let verifyURL = isSandbox ?
"https://sandbox.itunes.apple.com/verifyReceipt" :
"https://buy.itunes.apple.com/verifyReceipt"
// 发送验证请求...
}
- 错误处理与状态管理
- 统一的状态枚举
PurchaseState - 统一的错误消息展示
- 主线程安全的更新机制
使用说明
-
配置商品:
- 在 App Store Connect 中添加商品
- 设置唯一的商品 ID(如 "com.yourcompany.premium")
-
初始化管理器:
@StateObject private var storeManager = StoreManager() -
加载商品:
// 在视图出现时加载 .onAppear { storeManager.loadProducts() } -
处理购买:
Button(action: { storeManager.purchase(product) }) { // ... } -
处理恢复购买:
Button("恢复购买") { storeManager.restorePurchases() } -
服务器验证:
- 实现
sendReceiptToServer方法 - 在服务器端处理苹果的收据验证
- 实现
测试注意事项
-
沙盒测试:
- 在 App Store Connect 创建沙盒测试账号
- 使用沙盒环境验证收据
-
收据刷新:
// 强制刷新收据 let request = SKReceiptRefreshRequest() request.start() -
交易完成:
- 确保调用
finishTransaction()完成交易 - 避免重复处理同一交易
- 确保调用
-
订阅测试:
- 在 App Store Connect 修改订阅续订设置
- 测试不同续订状态的处理
此实现方案完全兼容 iOS 14 及以上版本,在 iOS 15+ 设备上使用现代 StoreKit 2 API,在 iOS 14 设备上回退到稳定可靠的 StoreKit 1 API,提供了无缝的用户体验和开发者接口。









网友评论