Files
swiftGrammar/AIGrammar/lib/IapManager.swift
2024-08-17 12:23:47 +08:00

234 lines
11 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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 apps 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 {
// NetworkManagerIapVerify
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) {
//
}
}