Initial commit
This commit is contained in:
@ -6,3 +6,228 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import StoreKit
|
||||
import SwiftUI
|
||||
import SwiftyBeaver
|
||||
|
||||
enum IAPProduct: String, CaseIterable {
|
||||
case premiumFeature1 = "grammar_1_week"
|
||||
case premiumFeature2 = "grammar_1_month"
|
||||
case premiumFeature3 = "grammar_1_year"
|
||||
|
||||
var weight: Int {
|
||||
switch self {
|
||||
case .premiumFeature1:
|
||||
return 1
|
||||
case .premiumFeature2:
|
||||
return 2
|
||||
case .premiumFeature3:
|
||||
return 3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class IAPManager: ObservableObject {
|
||||
@Published var products: [Product] = [] // 获取商品列表
|
||||
@Published var purchasedProducts: [Product] = [] // 已购买的商品列表
|
||||
private var processedTransactionIDs = Set<UInt64>() // 已经发到服务端校验的交易ID
|
||||
|
||||
init() {
|
||||
Task {
|
||||
await requestProducts()
|
||||
await updatePurchasedProducts()
|
||||
await listenForTransactions()
|
||||
}
|
||||
}
|
||||
|
||||
// 获取商品列表,并按照权重排序。
|
||||
func requestProducts() async {
|
||||
do {
|
||||
let products = try await Product.products(for: IAPProduct.allCases.map { $0.rawValue })
|
||||
DispatchQueue.main.async {
|
||||
// 按照产品的权重排序
|
||||
self.products = products.sorted { product1, product2 in
|
||||
let weight1 = IAPProduct(rawValue: product1.id)?.weight ?? 0
|
||||
let weight2 = IAPProduct(rawValue: product2.id)?.weight ?? 0
|
||||
return weight1 < weight2
|
||||
}
|
||||
|
||||
// 打印每个产品及其相关变量
|
||||
for product in self.products {
|
||||
if let receiptData = try? product.jsonRepresentation {
|
||||
if let jsonString = String(data: receiptData, encoding: .utf8) {
|
||||
logger.info("product details for \(product.id): \(jsonString)")
|
||||
} else {
|
||||
logger.info("product details for \(product.id): ", context: ["Tile": product.displayName, "Price": product.price, "DisplayPrice": product.displayPrice])
|
||||
}
|
||||
}else {
|
||||
logger.info("product details for \(product.id): ", context: ["Tile": product.displayName, "Price": product.price, "DisplayPrice": product.displayPrice])
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
logger.error("Failed to fetch products: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
// 商品购买
|
||||
func buy(product: Product, completion: @escaping (Result<String, Error>) -> Void) async {
|
||||
do {
|
||||
let result = try await product.purchase()
|
||||
switch result {
|
||||
case .success(let verification):
|
||||
if case .verified(let transaction) = verification {
|
||||
// 获取购买信息
|
||||
let appAccountToken = transaction.appAccountToken
|
||||
let productId = transaction.productID
|
||||
let transactionId = transaction.id
|
||||
|
||||
// 打印日志
|
||||
if let receiptData = try? transaction.jsonRepresentation {
|
||||
if let jsonString = String(data: receiptData, encoding: .utf8) {
|
||||
logger.info("Transaction details for \(transactionId): \(jsonString)")
|
||||
} else {
|
||||
logger.info("Transaction details for \(transactionId)", context: ["ProductID" : productId, "AppAccountToken" : appAccountToken ?? "", "OriTransactionID":transaction.originalID.value])
|
||||
}
|
||||
|
||||
} else {
|
||||
logger.info("Transaction details for \(transactionId)", context: ["ProductID" : productId, "AppAccountToken" : appAccountToken ?? "", "OriTransactionID":transaction.originalID.value])
|
||||
}
|
||||
|
||||
// 发送到服务端进行校验
|
||||
await validateReceipt(receiptData: transaction.jsonRepresentation, appAccountToken: appAccountToken, productId: productId, transactionId: transactionId, env: transaction.environment.rawValue)
|
||||
|
||||
await transaction.finish()
|
||||
await updatePurchasedProducts()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
// 购买成功的消息
|
||||
completion(.success("Purchase Successful"))
|
||||
}
|
||||
}
|
||||
case .userCancelled:
|
||||
// 用户取消支付,这个行为最好能报上去,或者做点其他营销动作
|
||||
logger.error("User cancelled the purchase.")
|
||||
|
||||
DispatchQueue.main.async {
|
||||
// 用户取消购买
|
||||
completion(.failure(NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Purchase Cancelled"])))
|
||||
}
|
||||
case .pending:
|
||||
// 需要上报上去
|
||||
logger.error("Purchase is pending.")
|
||||
|
||||
DispatchQueue.main.async {
|
||||
// 购买失败
|
||||
completion(.failure(NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Purchase Pending"])))
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
} catch {
|
||||
logger.error("Failed to purchase product: \(error.localizedDescription)")
|
||||
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://developer.apple.com/documentation/storekit/transaction/3851204-currententitlements
|
||||
// A sequence of the latest transactions that entitle a user to in-app purchases and subscriptions.
|
||||
func updatePurchasedProducts() async {
|
||||
for await result in Transaction.currentEntitlements {
|
||||
if case .verified(let transaction) = result {
|
||||
if let product = products.first(where: { $0.id == transaction.productID }) {
|
||||
DispatchQueue.main.async {
|
||||
self.purchasedProducts.append(product)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://developer.apple.com/documentation/storekit/transaction/3851206-updates
|
||||
// The asynchronous sequence that emits a transaction when the system creates or updates transactions that occur outside of the app or on other devices.
|
||||
// Note that after a successful in-app purchase on the same device, StoreKit returns the transaction through Product.PurchaseResult.success(_:).
|
||||
func listenForTransactions() async {
|
||||
for await transactionResult in Transaction.updates {
|
||||
if case .verified(let transaction) = transactionResult {
|
||||
if let product = products.first(where: { $0.id == transaction.productID }) {
|
||||
DispatchQueue.main.async {
|
||||
self.purchasedProducts.append(product)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取购买信息
|
||||
let appAccountToken = transaction.appAccountToken
|
||||
let productId = transaction.productID
|
||||
let transactionId = transaction.id
|
||||
|
||||
// 打印日志
|
||||
if let receiptData = try? transaction.jsonRepresentation {
|
||||
if let jsonString = String(data: receiptData, encoding: .utf8) {
|
||||
logger.info("Transaction details for \(transactionId): \(jsonString)")
|
||||
} else {
|
||||
logger.info("Transaction details for \(transactionId)", context: ["ProductID" : productId, "AppAccountToken" : appAccountToken ?? "", "OriTransactionID":transaction.originalID.value])
|
||||
}
|
||||
|
||||
} else {
|
||||
logger.info("Transaction details for \(transactionId)", context: ["ProductID" : productId, "AppAccountToken" : appAccountToken ?? "", "OriTransactionID":transaction.originalID.value])
|
||||
}
|
||||
|
||||
// 发送到服务端进行校验
|
||||
await validateReceipt(receiptData: transaction.jsonRepresentation, appAccountToken: appAccountToken, productId: productId, transactionId: transactionId, env: transaction.environment.rawValue)
|
||||
await transaction.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://developer.apple.com/documentation/storekit/appstore/3791906-sync
|
||||
// Synchronizes your app’s transaction information and subscription status with information from the App Store.
|
||||
func restorePurchases(completion: @escaping (Result<String, Error>) -> Void) async {
|
||||
do {
|
||||
try await AppStore.sync()
|
||||
await updatePurchasedProducts()
|
||||
completion(.success("Purchase Successful"))
|
||||
logger.info("Purchases restored")
|
||||
} catch {
|
||||
logger.error("Failed to restore purchases: \(error.localizedDescription)")
|
||||
completion(.failure(NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Purchase Pending"])))
|
||||
}
|
||||
}
|
||||
|
||||
// 服务端校验收据
|
||||
func validateReceipt(receiptData: Data, appAccountToken: UUID?, productId: String, transactionId: UInt64, env: String) async {
|
||||
// 调用NetworkManager中的IapVerify进行收据验证
|
||||
NetworkManager.shared.IapVerify(receiptData: receiptData, appAccountToken: appAccountToken, productId: productId, transactionId: transactionId, env: env) { result in
|
||||
switch result {
|
||||
case .success(let response):
|
||||
|
||||
DispatchQueue.main.async {
|
||||
logger.info("Receipt verification succeeded: \(response.ret)", context: ["ProductID":productId, "TransactionID":transactionId])
|
||||
|
||||
// 处理后续逻辑
|
||||
self.updatePurchasedStatus(productId: productId)
|
||||
}
|
||||
case .failure(let error):
|
||||
// 处理错误情况
|
||||
DispatchQueue.main.async {
|
||||
switch error {
|
||||
case .businessError(let ret, let message):
|
||||
logger.error("Business error - Ret: \(ret), Message: \(message)", context: ["ProductID":productId, "TransactionID":transactionId])
|
||||
case .other(let error):
|
||||
logger.error("network occurred: \(error.localizedDescription)", context: ["ProductID":productId, "TransactionID":transactionId])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 示例方法,用于更新购买状态
|
||||
private func updatePurchasedStatus(productId: String) {
|
||||
// 逻辑来标记产品为已购买,例如更新本地数据库、发送通知等
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user