234 lines
11 KiB
Swift
234 lines
11 KiB
Swift
//
|
||
// Iap.swift
|
||
// AIGrammar
|
||
//
|
||
// Created by oscar on 2024/7/3.
|
||
//
|
||
|
||
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) {
|
||
// 逻辑来标记产品为已购买,例如更新本地数据库、发送通知等
|
||
}
|
||
}
|
||
|