非消耗型内购的使用

在 iOS 应用中,非消耗型内购通常用于一次性购买永久解锁的功能或内容。以下是一个使用 SwiftUI 实现非消耗型内购的简单示例。


示例场景

假设我们有一个应用,用户可以通过内购永久解锁“高级功能”。


实现步骤

1. 在 App Store Connect 中配置内购项目

  • 登录 App Store Connect
  • 为你的应用创建一个非消耗型内购项目(例如,ID 为 com.yourapp.premium)。

2. 在 Xcode 中启用内购功能

  • 打开 Xcode 项目。
  • Signing & Capabilities 中添加 In-App Purchase 能力。

上面的代码使用的是 StoreKit 1,而不是最新的 StoreKit 2。StoreKit 2 是 Apple 在 WWDC 2021 推出的新版本,提供了更简洁、现代化的 API,并且完全基于 Swift 的异步编程模型(async/await)。

如果你希望使用 StoreKit 2 来实现非消耗型内购,以下是更新后的代码示例:


StoreKit 2 实现非消耗型内购

1. 配置内购项目

  • 在 App Store Connect 中创建非消耗型内购项目(例如,ID 为 com.yourapp.premium)。

2. 使用 StoreKit 2 实现内购逻辑

以下是基于 StoreKit 2 的 SwiftUI 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
import SwiftUI
import StoreKit

@MainActor
class StoreManager: ObservableObject {
@Published var products: [Product] = []
@Published var isPremiumUnlocked: Bool = false

private var updates: Task<Void, Never>? = nil

init() {
updates = observeTransactionUpdates()
fetchProducts()
checkUnlockedStatus()
}

deinit {
updates?.cancel()
}

// 获取内购产品
func fetchProducts() {
Task {
do {
let productIDs = ["com.yourapp.premium"] // 替换为你的内购 ID
products = try await Product.products(for: productIDs)
} catch {
print("Failed to fetch products: \(error)")
}
}
}

// 购买产品
func purchase(product: Product) {
Task {
do {
let result = try await product.purchase()
switch result {
case .success(let verification):
if case .verified(let transaction) = verification {
await transaction.finish()
unlockPremium()
}
case .pending:
print("Purchase is pending (e.g., waiting for parental approval)")
case .userCancelled:
print("User cancelled the purchase")
@unknown default:
break
}
} catch {
print("Purchase failed: \(error)")
}
}
}

// 恢复购买
func restorePurchases() {
Task {
do {
try await AppStore.sync()
checkUnlockedStatus()
} catch {
print("Failed to restore purchases: \(error)")
}
}
}

// 检查是否已解锁高级功能
func checkUnlockedStatus() {
Task {
let productIDs = ["com.yourapp.premium"] // 替换为你的内购 ID
for await result in Transaction.currentEntitlements(for: productIDs) {
if case .verified(let transaction) = result, transaction.revocationDate == nil {
DispatchQueue.main.async {
self.isPremiumUnlocked = true
UserDefaults.standard.set(true, forKey: "isPremiumUnlocked")
}
break
}
}
}
}

// 解锁高级功能
func unlockPremium() {
DispatchQueue.main.async {
self.isPremiumUnlocked = true
UserDefaults.standard.set(true, forKey: "isPremiumUnlocked")
}
}

// 监听交易更新
private func observeTransactionUpdates() -> Task<Void, Never> {
Task {
for await verification in Transaction.updates {
if case .verified(let transaction) = verification {
await transaction.finish()
checkUnlockedStatus()
}
}
}
}
}

struct ContentView: View {
@StateObject private var storeManager = StoreManager()

var body: some View {
VStack {
if storeManager.isPremiumUnlocked {
Text("高级功能已解锁!")
.font(.title)
.padding()
} else {
Text("解锁高级功能")
.font(.title)
.padding()

if let product = storeManager.products.first {
Button("购买 \(product.displayName) - \(product.displayPrice)") {
storeManager.purchase(product: product)
}
.padding()
}

Button("恢复购买") {
storeManager.restorePurchases()
}
.padding()
}
}
.onAppear {
// 检查是否已解锁高级功能
storeManager.isPremiumUnlocked = UserDefaults.standard.bool(forKey: "isPremiumUnlocked")
}
}
}

@main
struct InAppPurchaseDemoApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}

StoreKit 2 的核心改进

  1. 基于 async/await 的异步编程

    • StoreKit 2 完全支持 Swift 的 async/await,代码更简洁易读。
    • 例如,Product.products(for:)product.purchase() 都是异步方法。
  2. 简化的交易管理

    • 使用 Transaction.currentEntitlements(for:) 检查用户当前的购买状态。
    • 使用 Transaction.updates 监听交易更新。
  3. 自动验证交易

    • StoreKit 2 会自动验证交易,开发者无需手动处理收据验证。
  4. 更安全的 API

    • StoreKit 2 提供了更安全的 API,减少开发者的错误使用。

代码说明

  1. StoreManager:

    • 使用 @MainActor 确保 UI 更新在主线程执行。
    • 通过 Product.products(for:) 获取内购产品。
    • 使用 product.purchase() 发起购买。
    • 通过 Transaction.currentEntitlements(for:) 检查用户是否已购买。
  2. ContentView:

    • 显示 UI,包括购买按钮和恢复购买按钮。
    • 根据 isPremiumUnlocked 状态显示不同的内容。
  3. UserDefaults:

    • 用于持久化存储用户是否已解锁高级功能。

运行流程

  1. 应用启动时,检查是否已解锁高级功能。
  2. 显示购买按钮,用户可以点击购买。
  3. 购买成功后,解锁高级功能并更新 UI。
  4. 用户可以通过“恢复购买”按钮恢复之前的购买。

注意事项

  • 在真实应用中,建议将购买状态存储在服务器或使用更安全的方式(如 Keychain)。
  • 测试时,使用沙盒账户进行内购测试。
  • 确保在 App Store Connect 中正确配置内购项目。

如果你有其他问题,欢迎随时提问!

当然!以下是对你提供的代码的详细分析。我们将从代码结构、功能实现、潜在问题和改进建议等方面进行探讨。


代码结构分析

1. StoreManager

  • 职责:管理内购逻辑,包括获取产品、处理购买、恢复购买和检查购买状态。
  • 关键属性
    • products: [Product]:存储从 App Store 获取的内购产品。
    • isPremiumUnlocked: Bool:表示用户是否已解锁高级功能。
  • 关键方法
    • fetchProducts():异步获取内购产品。
    • purchase(product:):发起购买请求。
    • restorePurchases():恢复购买。
    • checkUnlockedStatus():检查用户是否已解锁高级功能。
    • unlockPremium():解锁高级功能并更新状态。
    • observeTransactionUpdates():监听交易更新。

2. ContentView 视图

  • 职责:显示 UI,包括购买按钮和恢复购买按钮。
  • 关键逻辑
    • 根据 isPremiumUnlocked 状态显示不同的内容。
    • 调用 StoreManager 的方法处理用户操作。

3. UserDefaults

  • 用途:持久化存储用户是否已解锁高级功能。

功能实现分析

1. 获取内购产品

  • 通过 Product.products(for:) 异步获取内购产品。
  • 将获取到的产品存储在 products 数组中。

2. 发起购买

  • 使用 product.purchase() 发起购买请求。
  • 处理购买结果:
    • 如果购买成功,调用 unlockPremium() 解锁高级功能。
    • 如果购买失败或用户取消,打印错误信息。

3. 恢复购买

  • 使用 AppStore.sync() 同步用户的购买记录。
  • 调用 checkUnlockedStatus() 检查用户是否已解锁高级功能。

4. 检查购买状态

  • 使用 Transaction.currentEntitlements(for:) 检查用户当前的购买状态。
  • 如果找到有效的交易记录,解锁高级功能。

5. 监听交易更新

  • 使用 Transaction.updates 监听交易更新。
  • 如果交易状态发生变化(例如退款或撤销),更新购买状态。

潜在问题分析

1. UserDefaults 的安全性

  • 问题UserDefaults 不是存储敏感数据(如购买状态)的安全方式。
  • 建议:使用更安全的方式(如 Keychain)存储购买状态。

2. 未处理网络错误

  • 问题:在 fetchProducts()purchase(product:) 中,网络错误可能导致功能异常。
  • 建议:添加更详细的错误处理逻辑,并向用户显示友好的错误提示。

3. 未处理退款或撤销

  • 问题:如果用户退款或撤销购买,代码中没有显式处理这种情况。
  • 建议:在 observeTransactionUpdates() 中检查 transaction.revocationDate,并根据需要锁定高级功能。

4. 未处理多产品场景

  • 问题:代码假设只有一个内购产品(com.yourapp.premium),无法处理多个产品。
  • 建议:扩展代码以支持多个产品,并根据产品 ID 动态更新 UI。

5. 未处理沙盒测试

  • 问题:代码中没有明确区分沙盒环境和生产环境。
  • 建议:在测试时,确保使用沙盒账户,并添加日志以帮助调试。

改进建议

1. 使用 Keychain 存储购买状态

  • 替换 UserDefaults,使用 Keychain 存储购买状态,提高安全性。

2. 增强错误处理

  • fetchProducts()purchase(product:) 中添加详细的错误处理逻辑。
  • 向用户显示友好的错误提示。

3. 处理退款或撤销

  • observeTransactionUpdates() 中检查 transaction.revocationDate,并根据需要锁定高级功能。

4. 支持多产品

  • 扩展代码以支持多个内购产品,并根据产品 ID 动态更新 UI。

5. 添加沙盒测试支持

  • 在测试时,确保使用沙盒账户,并添加日志以帮助调试。

改进后的代码示例

以下是改进后的部分代码:

使用 Keychain 存储购买状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import Security

func saveToKeychain(key: String, value: Bool) {
let data = Data(String(value).utf8)
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data
]
SecItemDelete(query as CFDictionary)
SecItemAdd(query as CFDictionary, nil)
}

func loadFromKeychain(key: String) -> Bool {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecReturnData as String: kCFBooleanTrue!,
kSecMatchLimit as String: kSecMatchLimitOne
]
var dataTypeRef: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)
if status == errSecSuccess, let data = dataTypeRef as? Data, let value = String(data: data, encoding: .utf8) {
return Bool(value) ?? false
}
return false
}

增强错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func purchase(product: Product) {
Task {
do {
let result = try await product.purchase()
switch result {
case .success(let verification):
if case .verified(let transaction) = verification {
await transaction.finish()
unlockPremium()
}
case .pending:
print("Purchase is pending (e.g., waiting for parental approval)")
case .userCancelled:
print("User cancelled the purchase")
@unknown default:
break
}
} catch {
print("Purchase failed: \(error)")
// 显示友好的错误提示
}
}
}

总结

你的代码已经实现了基本的内购功能,但可以通过以下方式进一步改进:

  1. 提高购买状态存储的安全性。
  2. 增强错误处理和用户提示。
  3. 支持多产品和处理退款/撤销。
  4. 优化沙盒测试支持。

如果你有其他问题或需要进一步的帮助,欢迎随时提问!


非消耗型内购的使用
http://asynch1889.github.io/2025/02/03/非消耗型内购的使用/
作者
John Doe
发布于
2025年2月3日
许可协议