Initial commit

This commit is contained in:
oscarz
2024-08-12 10:49:20 +08:00
parent 3002510aaf
commit 00fd0adf89
331 changed files with 53210 additions and 130 deletions

View File

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