mirror of
https://github.com/jaywcjlove/StoreKitHelper
synced 2026-04-21 13:37:20 +00:00
Add tri-state purchase status
This commit is contained in:
parent
d879dcc8eb
commit
2838432f56
4 changed files with 108 additions and 7 deletions
|
|
@ -56,10 +56,10 @@ struct PurchaseExample: View {
|
|||
Text("购买状态")
|
||||
.font(.headline)
|
||||
HStack {
|
||||
Image(systemName: store.hasPurchased ? "checkmark.circle.fill" : "xmark.circle.fill")
|
||||
.foregroundColor(store.hasPurchased ? .green : .red)
|
||||
Image(systemName: statusIconName)
|
||||
.foregroundColor(statusColor)
|
||||
|
||||
Text(store.hasPurchased ? "已购买" : "未购买")
|
||||
Text(statusText)
|
||||
.font(.subheadline)
|
||||
|
||||
Spacer()
|
||||
|
|
@ -99,7 +99,20 @@ struct PurchaseExample: View {
|
|||
Text("应用功能")
|
||||
.font(.headline)
|
||||
|
||||
if store.hasNotPurchased {
|
||||
if store.purchaseStatus == .loading {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("同步购买状态中")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Text("正在确认已购项目,请稍候")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding()
|
||||
.background(Color.secondary.opacity(0.08))
|
||||
.cornerRadius(8)
|
||||
} else if store.hasNotPurchased {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("🔒 受限功能")
|
||||
.font(.subheadline)
|
||||
|
|
@ -128,6 +141,39 @@ struct PurchaseExample: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var statusText: String {
|
||||
switch store.purchaseStatus {
|
||||
case .loading:
|
||||
"同步中"
|
||||
case .purchased:
|
||||
"已购买"
|
||||
case .notPurchased:
|
||||
"未购买"
|
||||
}
|
||||
}
|
||||
|
||||
private var statusIconName: String {
|
||||
switch store.purchaseStatus {
|
||||
case .loading:
|
||||
"clock.badge.questionmark"
|
||||
case .purchased:
|
||||
"checkmark.circle.fill"
|
||||
case .notPurchased:
|
||||
"xmark.circle.fill"
|
||||
}
|
||||
}
|
||||
|
||||
private var statusColor: Color {
|
||||
switch store.purchaseStatus {
|
||||
case .loading:
|
||||
.secondary
|
||||
case .purchased:
|
||||
.green
|
||||
case .notPurchased:
|
||||
.red
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -63,9 +63,14 @@ final class StoreKitNetworkErrorTests {
|
|||
func testStoreContextInitialization() async throws {
|
||||
let session = try makeSession()
|
||||
let store = await StoreContext(products: AppProduct.allCases)
|
||||
#expect(await store.purchaseStatus == .loading)
|
||||
#expect(await store.hasResolvedPurchaseStatus == false)
|
||||
#expect(await store.hasNotPurchased == false)
|
||||
try await Task.sleep(for: .milliseconds(500))
|
||||
#expect(await store.products.count == 2)
|
||||
#expect(await store.purchasedProductIDs.count == 0)
|
||||
#expect(await store.purchaseStatus == .notPurchased)
|
||||
#expect(await store.hasResolvedPurchaseStatus == true)
|
||||
#expect(await store.hasNotPurchased == true)
|
||||
#expect(await store.hasPurchased == false)
|
||||
let product = await store.products.first(where: { $0.id == AppProduct.lifetime.id })
|
||||
|
|
@ -86,6 +91,7 @@ final class StoreKitNetworkErrorTests {
|
|||
try await Task.sleep(for: .milliseconds(500))
|
||||
#expect(await store.products.count == 0)
|
||||
#expect(await store.purchasedProductIDs.count == 0)
|
||||
#expect(await store.purchaseStatus == .notPurchased)
|
||||
#expect(await store.hasNotPurchased == true)
|
||||
#expect(await store.hasPurchased == false)
|
||||
session.clearTransactions()
|
||||
|
|
@ -100,6 +106,7 @@ final class StoreKitNetworkErrorTests {
|
|||
try await Task.sleep(for: .milliseconds(500))
|
||||
#expect(await store.products.count == 2)
|
||||
#expect(await store.purchasedProductIDs.count == 0)
|
||||
#expect(await store.purchaseStatus == .notPurchased)
|
||||
#expect(await store.hasNotPurchased == true)
|
||||
#expect(await store.hasPurchased == false)
|
||||
let lifetime = await store.products.first(where: { $0.id == AppProduct.lifetime.id })
|
||||
|
|
@ -109,6 +116,7 @@ final class StoreKitNetworkErrorTests {
|
|||
session.disableDialogs = true
|
||||
await store.purchase(lifetime)
|
||||
}
|
||||
#expect(await store.purchaseStatus == .purchased)
|
||||
#expect(await store.hasPurchased == true)
|
||||
#expect(await store.purchasedProductIDs.contains(AppProduct.lifetime.id) == true)
|
||||
session.clearTransactions()
|
||||
|
|
@ -117,6 +125,7 @@ final class StoreKitNetworkErrorTests {
|
|||
#expect(await store.purchasedProductIDs.contains(AppProduct.lifetime.id) == true)
|
||||
session.disableDialogs = true
|
||||
await store.restorePurchases()
|
||||
#expect(await store.purchaseStatus == .notPurchased)
|
||||
#expect(await store.hasPurchased == false)
|
||||
session.clearTransactions()
|
||||
session.resetToDefaultState()
|
||||
|
|
@ -140,6 +149,7 @@ final class StoreKitNetworkErrorTests {
|
|||
}
|
||||
|
||||
// 验证购买成功
|
||||
#expect(await store.purchaseStatus == .purchased)
|
||||
#expect(await store.hasPurchased == true)
|
||||
#expect(await store.purchasedProductIDs.contains(AppProduct.monthly.id) == true)
|
||||
|
||||
|
|
@ -152,6 +162,7 @@ final class StoreKitNetworkErrorTests {
|
|||
|
||||
// 验证订阅已过期
|
||||
#expect(await store.purchasedProductIDs.contains(AppProduct.monthly.id) == false)
|
||||
#expect(await store.purchaseStatus == .notPurchased)
|
||||
#expect(await store.hasPurchased == false)
|
||||
|
||||
// 清理
|
||||
|
|
|
|||
|
|
@ -13,6 +13,16 @@ public protocol InAppProduct: CaseIterable, Identifiable where ID == ProductID {
|
|||
var id: ProductID { get }
|
||||
}
|
||||
|
||||
/// 购买状态
|
||||
public enum PurchaseStatus: Sendable, Equatable {
|
||||
/// 当前购买状态尚未完成首次同步
|
||||
case loading
|
||||
/// 当前存在有效购买
|
||||
case purchased
|
||||
/// 当前不存在有效购买
|
||||
case notPurchased
|
||||
}
|
||||
|
||||
// MARK: - StoreContext
|
||||
|
||||
/// StoreKit 上下文,用于管理应用内购买状态
|
||||
|
|
@ -23,6 +33,8 @@ public final class StoreContext: ObservableObject {
|
|||
@Published public private(set) var products: [Product] = []
|
||||
/// 购买的产品标识符集合
|
||||
@Published public private(set) var purchasedProductIDs: Set<String> = []
|
||||
/// 当前购买状态
|
||||
@Published public private(set) var purchaseStatus: PurchaseStatus = .loading
|
||||
/// 是否正在加载
|
||||
@Published public private(set) var isLoading = false
|
||||
/// 错误信息
|
||||
|
|
@ -35,12 +47,17 @@ public final class StoreContext: ObservableObject {
|
|||
|
||||
/// 用户是否没有购买任何产品
|
||||
public var hasNotPurchased: Bool {
|
||||
purchasedProductIDs.isEmpty
|
||||
purchaseStatus == .notPurchased
|
||||
}
|
||||
|
||||
/// 用户是否已购买任何产品
|
||||
public var hasPurchased: Bool {
|
||||
!purchasedProductIDs.isEmpty
|
||||
purchaseStatus == .purchased
|
||||
}
|
||||
|
||||
/// 用户购买状态是否已完成首次同步
|
||||
public var hasResolvedPurchaseStatus: Bool {
|
||||
purchaseStatus != .loading
|
||||
}
|
||||
public let productIDs: [String]
|
||||
|
||||
|
|
@ -84,6 +101,7 @@ public final class StoreContext: ObservableObject {
|
|||
if let transaction = checkVerified(verificationResult) {
|
||||
// 更新购买状态
|
||||
purchasedProductIDs.insert(transaction.productID)
|
||||
purchaseStatus = .purchased
|
||||
// 完成交易
|
||||
await transaction.finish()
|
||||
|
||||
|
|
@ -188,6 +206,7 @@ public final class StoreContext: ObservableObject {
|
|||
if let transaction = checkVerified(verificationResult) {
|
||||
// 更新购买状态
|
||||
purchasedProductIDs.insert(transaction.productID)
|
||||
purchaseStatus = .purchased
|
||||
// 完成交易
|
||||
await transaction.finish()
|
||||
}
|
||||
|
|
@ -246,7 +265,23 @@ public final class StoreContext: ObservableObject {
|
|||
|
||||
await MainActor.run {
|
||||
self.purchasedProductIDs = purchasedIDs
|
||||
self.purchaseStatus = purchasedIDs.isEmpty ? .notPurchased : .purchased
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
extension StoreContext {
|
||||
func _setPurchasedProductIDsForTesting(_ productIDs: Set<String>) {
|
||||
purchasedProductIDs = productIDs
|
||||
purchaseStatus = productIDs.isEmpty ? .notPurchased : .purchased
|
||||
}
|
||||
|
||||
func _setPurchaseStatusForTesting(_ status: PurchaseStatus) {
|
||||
purchaseStatus = status
|
||||
if status != .purchased {
|
||||
purchasedProductIDs = []
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -32,7 +32,9 @@ func testStoreContextInitialization() async throws {
|
|||
await MainActor.run {
|
||||
#expect(store.products.isEmpty) // 初始时产品列表为空(需要从 App Store 加载)
|
||||
#expect(store.purchasedProductIDs.isEmpty)
|
||||
#expect(store.hasNotPurchased == true)
|
||||
#expect(store.purchaseStatus == .loading)
|
||||
#expect(store.hasResolvedPurchaseStatus == false)
|
||||
#expect(store.hasNotPurchased == false)
|
||||
#expect(store.hasPurchased == false)
|
||||
}
|
||||
}
|
||||
|
|
@ -43,13 +45,20 @@ func testPurchaseStatusMethods() async throws {
|
|||
|
||||
await MainActor.run {
|
||||
// 测试购买状态检查方法
|
||||
#expect(store.purchaseStatus == .loading)
|
||||
#expect(store.isPurchased("test.basic") == false)
|
||||
#expect(store.isPurchased(TestProduct.premium) == false)
|
||||
store._setPurchaseStatusForTesting(.notPurchased)
|
||||
#expect(store.purchaseStatus == .notPurchased)
|
||||
#expect(store.hasResolvedPurchaseStatus == true)
|
||||
#expect(store.hasNotPurchased == true)
|
||||
#expect(store.hasPurchased == false)
|
||||
// 模拟购买状态(仅在测试中使用)
|
||||
store._setPurchasedProductIDsForTesting(["test.basic"])
|
||||
#expect(store.isPurchased("test.basic") == true)
|
||||
#expect(store.isPurchased(TestProduct.basic) == true)
|
||||
#expect(store.isPurchased("test.premium") == false)
|
||||
#expect(store.purchaseStatus == .purchased)
|
||||
#expect(store.hasPurchased == true)
|
||||
#expect(store.hasNotPurchased == false)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue