From ac6a3f61b1da4dc25074cd9ee14985ee0e31a6f5 Mon Sep 17 00:00:00 2001 From: Sergey Korney Date: Tue, 1 Oct 2024 18:01:55 +0500 Subject: [PATCH] 1.6.41 --- AffiseAttributionLib.podspec | 2 +- .../Classes/internal/BuildConfig.swift | 2 +- .../subscription/AffiseProductPrice.swift | 4 - .../AffiseSubscriptionError.swift | 2 + .../Classes/utils/Array+Serialized.swift | 4 + AffiseInternal.podspec | 2 +- AffiseInternal/Classes/data/DataMapper.swift | 4 +- AffiseModule.podspec | 2 +- .../Classes/AdvertisingModule.swift | 2 +- AffiseModule/Link/Classes/LinkModule.swift | 2 +- .../Status/Classes/StatusModule.swift | 2 +- .../Classes/SubscriptionModule.swift | 2 +- .../Classes/store/StoreManager.swift | 16 ++- .../Classes/store/base/ProductManager.swift | 9 ++ .../store/base/TransactionManager.swift | 7 ++ .../Classes/store/event/OperationEvent.swift | 78 +++++++++---- ...tManager.swift => ProductManagerSK1.swift} | 39 ++++--- ...ager.swift => TransactionManagerSK1.swift} | 57 ++++----- .../store/storekit2/ProductManagerSK2.swift | 51 ++++++++ .../storekit2/TransactionManagerSK2.swift | 110 ++++++++++++++++++ .../Classes/utils/AffiseProductExt.swift | 5 + .../utils/AffisePurchasedInfoExt.swift | 12 ++ .../Classes/utils/StoreKit2Ext.swift | 81 +++++++++++++ AffiseSKAdNetwork.podspec | 2 +- CHANGELOG.md | 7 ++ README.md | 44 +++---- 26 files changed, 445 insertions(+), 103 deletions(-) create mode 100644 AffiseModule/Subscription/Classes/store/base/ProductManager.swift create mode 100644 AffiseModule/Subscription/Classes/store/base/TransactionManager.swift rename AffiseModule/Subscription/Classes/store/storekit/{ProductManager.swift => ProductManagerSK1.swift} (93%) rename AffiseModule/Subscription/Classes/store/storekit/{TransactionManager.swift => TransactionManagerSK1.swift} (94%) create mode 100644 AffiseModule/Subscription/Classes/store/storekit2/ProductManagerSK2.swift create mode 100644 AffiseModule/Subscription/Classes/store/storekit2/TransactionManagerSK2.swift create mode 100644 AffiseModule/Subscription/Classes/utils/StoreKit2Ext.swift diff --git a/AffiseAttributionLib.podspec b/AffiseAttributionLib.podspec index b11375f..e00906b 100644 --- a/AffiseAttributionLib.podspec +++ b/AffiseAttributionLib.podspec @@ -5,7 +5,7 @@ Pod::Spec.new do |spec| spec.name = "AffiseAttributionLib" - spec.version = ENV['LIB_VERSION'] || "1.6.40" + spec.version = ENV['LIB_VERSION'] || "1.6.41" spec.summary = "Affise Attribution iOS library" spec.description = "Affise SDK is a software you can use to collect app usage statistics, device identifiers, deeplink usage, track install referrer." spec.homepage = "https://github.com/affise/sdk-ios" diff --git a/AffiseAttributionLib/Classes/internal/BuildConfig.swift b/AffiseAttributionLib/Classes/internal/BuildConfig.swift index 0416d76..90e4a7d 100644 --- a/AffiseAttributionLib/Classes/internal/BuildConfig.swift +++ b/AffiseAttributionLib/Classes/internal/BuildConfig.swift @@ -1,5 +1,5 @@ import Foundation internal struct BuildConfig { - static let AFFISE_VERSION = "1.6.40" + static let AFFISE_VERSION = "1.6.41" } diff --git a/AffiseAttributionLib/Classes/modules/subscription/AffiseProductPrice.swift b/AffiseAttributionLib/Classes/modules/subscription/AffiseProductPrice.swift index 311912f..b742877 100644 --- a/AffiseAttributionLib/Classes/modules/subscription/AffiseProductPrice.swift +++ b/AffiseAttributionLib/Classes/modules/subscription/AffiseProductPrice.swift @@ -29,10 +29,6 @@ public class AffiseProductPrice: NSObject { formatter.numberStyle = .currency let currencyCode = locale.currencyCode ?? "" - - // if #available(iOS 16, *) { - // currencyCode = locale.currency?.identifier ?? "" // TODO - // } self.init( value: value, diff --git a/AffiseAttributionLib/Classes/modules/subscription/AffiseSubscriptionError.swift b/AffiseAttributionLib/Classes/modules/subscription/AffiseSubscriptionError.swift index dbd01dc..664b2c5 100644 --- a/AffiseAttributionLib/Classes/modules/subscription/AffiseSubscriptionError.swift +++ b/AffiseAttributionLib/Classes/modules/subscription/AffiseSubscriptionError.swift @@ -5,6 +5,7 @@ public enum AffiseSubscriptionError: Error { case notInitialized case productNotFound([String]) case purchaseFailed(Error?) + case userCancelled } extension AffiseSubscriptionError: CustomStringConvertible { @@ -13,6 +14,7 @@ extension AffiseSubscriptionError: CustomStringConvertible { case .notInitialized: return "affise not initialized" case .productNotFound(let ids): return "product not found [\(ids.joined(separator: ", "))]" case .purchaseFailed(let error): return "purchase failed: \(error?.localizedDescription ?? "")" + case .userCancelled: return "user cancelled" } } } diff --git a/AffiseAttributionLib/Classes/utils/Array+Serialized.swift b/AffiseAttributionLib/Classes/utils/Array+Serialized.swift index b7170f2..a90b7af 100644 --- a/AffiseAttributionLib/Classes/utils/Array+Serialized.swift +++ b/AffiseAttributionLib/Classes/utils/Array+Serialized.swift @@ -15,6 +15,10 @@ public extension Array where Element == (String, Any?) { return "\"\(key)\":[\"\(value.joined(separator: "\",\""))\"]" } else if let value = value as? [String] { return "\"\(key)\":[\(value.joined(separator: ","))]" + } else if let value = value as? (String, Any?) { + return "\"\(key)\":\([value].jsonString())" +// } else if let value = value as? [String: Any?] { +// return "\"\(key)\":\(value.jsonString())" } else if let value = value as? [(String, Any?)] { return "\"\(key)\":\(value.jsonString())" } else if let value = value as? [[(String, Any?)]] { diff --git a/AffiseInternal.podspec b/AffiseInternal.podspec index fdf2699..667ca62 100644 --- a/AffiseInternal.podspec +++ b/AffiseInternal.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |spec| spec.name = "AffiseInternal" - spec.version = ENV['LIB_VERSION'] || "1.6.40" + spec.version = ENV['LIB_VERSION'] || "1.6.41" spec.summary = "Affise Internal library" spec.description = "Affise Internal wrapper library for crossplatform" spec.homepage = "https://github.com/affise/sdk-ios" diff --git a/AffiseInternal/Classes/data/DataMapper.swift b/AffiseInternal/Classes/data/DataMapper.swift index d75193b..29a0621 100644 --- a/AffiseInternal/Classes/data/DataMapper.swift +++ b/AffiseInternal/Classes/data/DataMapper.swift @@ -64,7 +64,7 @@ public class DataMapper { DataName.PRODUCT: fromProduct(data.product), DataName.ORDER_ID: data.orderId, DataName.ORIGINAL_ORDER_ID: data.originalOrderId, -// DataName.PURCHASE: (data.purchase as? CustomStringConvertible)?.description, // TODO +// DataName.PURCHASE: data.purchase, ] } @@ -81,7 +81,7 @@ public class DataMapper { DataName.PRODUCT_TYPE: product.productType?.enumValue, DataName.PRICE: fromPrice(product.price), DataName.SUBSCRIPTION: fromSubscription(product.subscription), -// DataName.PRODUCT_DETAILS: (product.productDetails as? CustomStringConvertible)?.description, // TODO +// DataName.PRODUCT_DETAILS: product.productDetails, ] } diff --git a/AffiseModule.podspec b/AffiseModule.podspec index a51bd14..85c566c 100644 --- a/AffiseModule.podspec +++ b/AffiseModule.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = "AffiseModule" - s.version = ENV["LIB_VERSION"] || "1.6.40" + s.version = ENV["LIB_VERSION"] || "1.6.41" s.summary = "Affise Modules" s.description = "Affise module collection" s.homepage = "https://github.com/affise/sdk-ios" diff --git a/AffiseModule/Advertising/Classes/AdvertisingModule.swift b/AffiseModule/Advertising/Classes/AdvertisingModule.swift index 8c05082..b899980 100644 --- a/AffiseModule/Advertising/Classes/AdvertisingModule.swift +++ b/AffiseModule/Advertising/Classes/AdvertisingModule.swift @@ -4,7 +4,7 @@ import Foundation @objc(AffiseAdvertisingModule) public final class AdvertisingModule: AffiseModule { - public override var version: String { "1.6.40" } + public override var version: String { "1.6.41" } private lazy var advertisingIdManager: AdvertisingIdManager = AdvertisingIdManagerImpl() diff --git a/AffiseModule/Link/Classes/LinkModule.swift b/AffiseModule/Link/Classes/LinkModule.swift index c577560..d0e642b 100644 --- a/AffiseModule/Link/Classes/LinkModule.swift +++ b/AffiseModule/Link/Classes/LinkModule.swift @@ -5,7 +5,7 @@ import AffiseAttributionLib @objc(AffiseLinkModule) public final class LinkModule: AffiseModule { - public override var version: String { "1.6.40" } + public override var version: String { "1.6.41" } private var useCase: LinkResolveUseCase? = nil diff --git a/AffiseModule/Status/Classes/StatusModule.swift b/AffiseModule/Status/Classes/StatusModule.swift index b1913d7..ad18ac1 100644 --- a/AffiseModule/Status/Classes/StatusModule.swift +++ b/AffiseModule/Status/Classes/StatusModule.swift @@ -5,7 +5,7 @@ import AffiseAttributionLib @objc(AffiseStatusModule) public final class StatusModule: AffiseModule { - public override var version: String { "1.6.40" } + public override var version: String { "1.6.41" } private var checkStatusUseCase: CheckStatusUseCase? = nil private var referrerUseCase: ReferrerUseCase? = nil diff --git a/AffiseModule/Subscription/Classes/SubscriptionModule.swift b/AffiseModule/Subscription/Classes/SubscriptionModule.swift index 66259aa..40a8b04 100644 --- a/AffiseModule/Subscription/Classes/SubscriptionModule.swift +++ b/AffiseModule/Subscription/Classes/SubscriptionModule.swift @@ -6,7 +6,7 @@ import AffiseAttributionLib @objc(AffiseSubscriptionModule) public final class SubscriptionModule: AffiseModule { - public override var version: String { "1.6.40" } + public override var version: String { "1.6.41" } lazy var storeManager: StoreManager = StoreManager() diff --git a/AffiseModule/Subscription/Classes/store/StoreManager.swift b/AffiseModule/Subscription/Classes/store/StoreManager.swift index 988d110..7b84486 100644 --- a/AffiseModule/Subscription/Classes/store/StoreManager.swift +++ b/AffiseModule/Subscription/Classes/store/StoreManager.swift @@ -4,8 +4,20 @@ import AffiseAttributionLib internal class StoreManager: NSObject { - lazy var productManager: ProductManager = ProductManager() - lazy var transactionManager: TransactionManager = TransactionManager(productManager: productManager) + lazy var productManager: ProductManager = { + if #available(iOS 15.0, *) { + return ProductManagerSK2() + } + return ProductManagerSK1() + }() + + lazy var transactionManager: TransactionManager = { + if #available(iOS 15.0, *) { + return TransactionManagerSK2(productManager: productManager) + } + return TransactionManagerSK1(productManager: productManager) + }() + func fetchProducts(_ productsIds: [String], _ callback: @escaping AffiseResultCallback) { productManager.fetchProducts(productsIds, callback) diff --git a/AffiseModule/Subscription/Classes/store/base/ProductManager.swift b/AffiseModule/Subscription/Classes/store/base/ProductManager.swift new file mode 100644 index 0000000..2368916 --- /dev/null +++ b/AffiseModule/Subscription/Classes/store/base/ProductManager.swift @@ -0,0 +1,9 @@ +import Foundation +import StoreKit +import AffiseAttributionLib + +protocol ProductManager { + func fetchProducts(_ productsIds: [String], _ callback: @escaping AffiseResultCallback) + + func product(_ id: String) -> Any +} diff --git a/AffiseModule/Subscription/Classes/store/base/TransactionManager.swift b/AffiseModule/Subscription/Classes/store/base/TransactionManager.swift new file mode 100644 index 0000000..2c1a0fb --- /dev/null +++ b/AffiseModule/Subscription/Classes/store/base/TransactionManager.swift @@ -0,0 +1,7 @@ +import Foundation +import StoreKit +import AffiseAttributionLib + +protocol TransactionManager { + func purchase(_ product: AffiseProduct, _ type: AffiseProductType?, _ callback: @escaping AffiseResultCallback) +} diff --git a/AffiseModule/Subscription/Classes/store/event/OperationEvent.swift b/AffiseModule/Subscription/Classes/store/event/OperationEvent.swift index df2b750..11b929b 100644 --- a/AffiseModule/Subscription/Classes/store/event/OperationEvent.swift +++ b/AffiseModule/Subscription/Classes/store/event/OperationEvent.swift @@ -3,14 +3,60 @@ import StoreKit import AffiseAttributionLib internal enum OperationEvent { + static func create(_ transaction: SKPaymentTransaction, product: SKProduct, type: AffiseProductType?, failed: Bool) -> Event? { + var timeUnit = "" + var numberOfUnits: Int64 = 0 + + if #available(iOS 11.2, *) { + timeUnit = product.subscriptionPeriod?.unitType.enumValue ?? "" + numberOfUnits = Int64(product.subscriptionPeriod?.numberOfUnits ?? 0) + } + + return create( + orderId: transaction.transactionIdentifier, + originalOrderId: transaction.original?.transactionIdentifier, + productId: product.productIdentifier, + timeUnit: timeUnit, + numberOfUnits: numberOfUnits, + price: product.price.floatValue, + currency: product.priceLocale.currencyCode ?? "", + + type: type ?? product.toAffiseSubscriptionType ?? .CONSUMABLE, + failed: failed + ) + } + + @available(iOS 15, *) + static func create(_ transaction: Transaction?, product: Product, type: AffiseProductType?, failed: Bool) -> Event? { + var timeUnit = product.subscription?.subscriptionPeriod.unitType.enumValue ?? "" + var numberOfUnits: Int = product.subscription?.subscriptionPeriod.value ?? 0 + var orderId: String? = nil + var originalOrderId: String? = nil + + if let transaction = transaction { + orderId = "\(transaction.id)" + originalOrderId = "\(transaction.originalID)" + } + + return create( + orderId: orderId, + originalOrderId: originalOrderId, + productId: "\(product.id)", + timeUnit: timeUnit, + numberOfUnits: Int64(numberOfUnits), + price: NSDecimalNumber(decimal: product.price).floatValue, + currency: product.priceFormatStyle.currencyCode, + + type: product.toAffiseSubscriptionType ?? type ?? .CONSUMABLE, + failed: failed + ) + } + + static func create(orderId: String?, originalOrderId: String?, productId: String, timeUnit: String, numberOfUnits: Int64, price: Float, currency: String, type: AffiseProductType, failed: Bool) -> Event? { var event: Event? - let eventType: AffiseProductType = type ?? product.toAffiseSubscriptionType ?? .CONSUMABLE - - let orderId: String? = transaction.transactionIdentifier - let originalOrderId: String? = transaction.original?.transactionIdentifier - - switch eventType { + + switch type { case .CONSUMABLE, .NON_CONSUMABLE: if failed { event = AfFailedPurchaseEvent() @@ -18,8 +64,8 @@ internal enum OperationEvent { event = AfPurchaseEvent() } let _ = event? - .addPredefinedParameter(.PRODUCT_ID, string: product.productIdentifier) - .addPredefinedParameter(.PRODUCT_TYPE, string: eventType.enumValue) + .addPredefinedParameter(.PRODUCT_ID, string: productId) + .addPredefinedParameter(.PRODUCT_TYPE, string: type.enumValue) case .RENEWABLE_SUBSCRIPTION, .NON_RENEWABLE_SUBSCRIPTION: if failed { @@ -32,17 +78,9 @@ internal enum OperationEvent { } } - var timeUnit = "" - var numberOfUnits: Int64 = 0 - - if #available(iOS 11.2, *) { - timeUnit = product.subscriptionPeriod?.unitType.enumValue ?? "" - numberOfUnits = Int64(product.subscriptionPeriod?.numberOfUnits ?? 0) - } - let _ = event? - .addPredefinedParameter(.SUBSCRIPTION_ID, string: product.productIdentifier) - .addPredefinedParameter(.SUBSCRIPTION_TYPE, string: eventType.enumValue) + .addPredefinedParameter(.SUBSCRIPTION_ID, string: productId) + .addPredefinedParameter(.SUBSCRIPTION_TYPE, string: type.enumValue) .addPredefinedParameter(.UNIT, string: timeUnit) .addPredefinedParameter(.QUANTITY, long: numberOfUnits) @@ -52,7 +90,7 @@ internal enum OperationEvent { return event? .addPredefinedParameter(.ORDER_ID, string: orderId ?? "") .addPredefinedParameter(.ORIGINAL_ORDER_ID, string: originalOrderId ?? "") - .addPredefinedParameter(.CURRENCY, string: product.priceLocale.currencyCode ?? "") - .addPredefinedParameter(.PRICE, float: product.price.floatValue) + .addPredefinedParameter(.CURRENCY, string: currency) + .addPredefinedParameter(.PRICE, float: price) } } diff --git a/AffiseModule/Subscription/Classes/store/storekit/ProductManager.swift b/AffiseModule/Subscription/Classes/store/storekit/ProductManagerSK1.swift similarity index 93% rename from AffiseModule/Subscription/Classes/store/storekit/ProductManager.swift rename to AffiseModule/Subscription/Classes/store/storekit/ProductManagerSK1.swift index 4f06eeb..aae9c68 100644 --- a/AffiseModule/Subscription/Classes/store/storekit/ProductManager.swift +++ b/AffiseModule/Subscription/Classes/store/storekit/ProductManagerSK1.swift @@ -3,7 +3,7 @@ import StoreKit import AffiseAttributionLib -internal class ProductManager: NSObject { +internal class ProductManagerSK1: NSObject { private let queue = DispatchQueue(label: "Affise.ProductFetcher") @@ -12,20 +12,6 @@ internal class ProductManager: NSObject { private var callbacks: [[String]: [AffiseResultCallback]] = [:] private var requests: [SKRequest:[String]] = [:] - - func fetchProducts(_ productsIds: [String], _ callback: @escaping AffiseResultCallback) { - queue.async { [weak self] in - guard let self = self else { return } - - if let callbacks = self.callbacks[productsIds] { - self.callbacks[productsIds] = callbacks + [callback] - return - } - - self.callbacks[productsIds] = [callback] - self.productsRequest(productsIds) - } - } func productsRequest(_ ids: [String]) { let request = SKProductsRequest(productIdentifiers: Set(ids)) @@ -36,7 +22,7 @@ internal class ProductManager: NSObject { } -extension ProductManager: SKProductsRequestDelegate { +extension ProductManagerSK1: SKProductsRequestDelegate { func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { queue.async { [weak self] in @@ -99,3 +85,24 @@ extension ProductManager: SKProductsRequestDelegate { return (ids, handlers) } } + +extension ProductManagerSK1: ProductManager { + + func fetchProducts(_ productsIds: [String], _ callback: @escaping AffiseResultCallback) { + queue.async { [weak self] in + guard let self = self else { return } + + if let callbacks = self.callbacks[productsIds] { + self.callbacks[productsIds] = callbacks + [callback] + return + } + + self.callbacks[productsIds] = [callback] + self.productsRequest(productsIds) + } + } + + func product(_ id: String) -> Any { + return self.products[id] + } +} diff --git a/AffiseModule/Subscription/Classes/store/storekit/TransactionManager.swift b/AffiseModule/Subscription/Classes/store/storekit/TransactionManagerSK1.swift similarity index 94% rename from AffiseModule/Subscription/Classes/store/storekit/TransactionManager.swift rename to AffiseModule/Subscription/Classes/store/storekit/TransactionManagerSK1.swift index fa067d2..c63ff82 100644 --- a/AffiseModule/Subscription/Classes/store/storekit/TransactionManager.swift +++ b/AffiseModule/Subscription/Classes/store/storekit/TransactionManagerSK1.swift @@ -2,7 +2,7 @@ import Foundation import StoreKit import AffiseAttributionLib -internal class TransactionManager: NSObject { +internal class TransactionManagerSK1: NSObject { private let queue = DispatchQueue(label: "Affise.TransactionManager") @@ -14,31 +14,8 @@ internal class TransactionManager: NSObject { init(productManager: ProductManager) { super.init() - SKPaymentQueue.default().add(self) self.productManager = productManager - } - - func purchase(_ product: AffiseProduct, _ type: AffiseProductType?, _ callback: @escaping AffiseResultCallback) { - queue.async { [weak self] in - guard let self = self else { return } - let productId = product.productId - - guard let product = self.productManager?.products[productId] else { - callback(.failure(AffiseSubscriptionError.productNotFound([productId]))) - return - } - - if let (callbacks, type) = self.callbacks[productId] { - self.callbacks[productId] = (callbacks + [callback], type) - return - } - - self.callbacks[productId] = ([callback], type) - self.transactionProducts[productId] = (product, type) - - let payment = SKPayment(product: product) - SKPaymentQueue.default().add(payment) - } + SKPaymentQueue.default().add(self) } func failedTransaction(_ transaction: SKPaymentTransaction) { @@ -48,7 +25,6 @@ internal class TransactionManager: NSObject { let productId = transaction.payment.productIdentifier guard let (product, type) = self.removeTransactionByProduct(productId) else { - // TODO return } @@ -66,7 +42,6 @@ internal class TransactionManager: NSObject { let productId = transaction.payment.productIdentifier guard let (product, type) = self.removeTransactionByProduct(productId) else { - // TODO return } @@ -99,7 +74,7 @@ internal class TransactionManager: NSObject { } -extension TransactionManager: SKPaymentTransactionObserver { +extension TransactionManagerSK1: SKPaymentTransactionObserver { func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { for transaction in transactions { switch transaction.transactionState { @@ -135,3 +110,29 @@ extension TransactionManager: SKPaymentTransactionObserver { } } } + +extension TransactionManagerSK1: TransactionManager { + + func purchase(_ product: AffiseProduct, _ type: AffiseProductType?, _ callback: @escaping AffiseResultCallback) { + queue.async { [weak self] in + guard let self = self else { return } + let productId = product.productId + + guard let product = self.productManager?.product(productId) as? SKProduct else { + callback(.failure(AffiseSubscriptionError.productNotFound([productId]))) + return + } + + if let (callbacks, type) = self.callbacks[productId] { + self.callbacks[productId] = (callbacks + [callback], type) + return + } + + self.callbacks[productId] = ([callback], type) + self.transactionProducts[productId] = (product, type) + + let payment = SKPayment(product: product) + SKPaymentQueue.default().add(payment) + } + } +} diff --git a/AffiseModule/Subscription/Classes/store/storekit2/ProductManagerSK2.swift b/AffiseModule/Subscription/Classes/store/storekit2/ProductManagerSK2.swift new file mode 100644 index 0000000..8f90c25 --- /dev/null +++ b/AffiseModule/Subscription/Classes/store/storekit2/ProductManagerSK2.swift @@ -0,0 +1,51 @@ +import Foundation +import StoreKit +import AffiseAttributionLib + + +@available(iOS 15, *) +internal class ProductManagerSK2: NSObject { + + private(set) var products: [String: Product] = [:] + + func getInvalid(_ productsIds: [String]) -> [String] { + let valid = self.products.keys + return productsIds.filter { id in + !valid.contains(id) + } + } + + func updateProducts(_ skProducts: [Product]) -> [AffiseProduct] { + self.products = skProducts.reduce(into: [:]) { dict, product in + dict[product.id] = product + } + return self.products.map { $0.value.toAffiseProduct() } + } + + func fetch(_ productsIds: [String]) async -> AffiseResult { + do { + let skProducts = try await Product.products(for: productsIds) + let products: [AffiseProduct] = updateProducts(skProducts) + let invalid = getInvalid(productsIds) + return .success(AffiseProductsResult(products: products, invalid: invalid)) + } catch { + return .failure(error) + } + return .failure(AffiseSubscriptionError.productNotFound(productsIds)) + } +} + +@available(iOS 15, *) +extension ProductManagerSK2: ProductManager { + + func fetchProducts(_ productsIds: [String], _ callback: @escaping AffiseResultCallback) { + Task { + let result = await self.fetch(productsIds) + callback(result) + } + } + + func product(_ id: String) -> Any { + return self.products[id] + } +} diff --git a/AffiseModule/Subscription/Classes/store/storekit2/TransactionManagerSK2.swift b/AffiseModule/Subscription/Classes/store/storekit2/TransactionManagerSK2.swift new file mode 100644 index 0000000..d224c4d --- /dev/null +++ b/AffiseModule/Subscription/Classes/store/storekit2/TransactionManagerSK2.swift @@ -0,0 +1,110 @@ +import Foundation +import StoreKit +import AffiseAttributionLib + + +@available(iOS 15, *) +internal class TransactionManagerSK2: NSObject { + + private var productManager: ProductManager? = nil + + private var purchasedProducts: Set = [] + + init(productManager: ProductManager) { + super.init() + self.productManager = productManager + + self.transactionsUpdates() + } + + func transactionsUpdates() { + Task { + for await update in Transaction.updates { + guard case .verified(let transaction) = update else { + continue + } + + await updatePurchases() + await transaction.finish() + + if let product = self.getProduct(transaction.productID) { + self.onSuccess(transaction, product) + } + } + } + } + + func getProduct(_ productId: String) -> Product? { + return self.productManager?.product(productId) as? Product + } + + func updatePurchases() async { + for await result in Transaction.currentEntitlements { + guard let transaction = try? result.payloadValue else { + continue + } + + if transaction.revocationDate == nil { + purchasedProducts.insert(transaction.productID) + } else { + purchasedProducts.remove(transaction.productID) + } + } + } + + func onFailed(_ transaction: Transaction?, _ product: Product, _ type: AffiseProductType? = nil) { + let productType = product.toAffiseSubscriptionType ?? type + OperationEvent.create(transaction, product: product, type: productType, failed: true)? + .send() + } + + func onSuccess(_ transaction: Transaction, _ product: Product, _ type: AffiseProductType? = nil) { + let productType = product.toAffiseSubscriptionType ?? type + OperationEvent.create(transaction, product: product, type: productType, failed: false)? + .send() + } + + func purchase(_ product: Product, _ type: AffiseProductType?) async -> AffiseResult { + do { + let result = try await product.purchase() + switch result { + case .success(.verified(let transaction)): + await transaction.finish() + onSuccess(transaction, product, type) + return .success(AffisePurchasedInfo(transaction, product.toAffiseProduct(type))) + case .success(.unverified(let transaction, let error)): + onSuccess(transaction, product, type) + return .success(AffisePurchasedInfo(transaction, product.toAffiseProduct(type))) + case .pending: + break + case .userCancelled: + onFailed(nil, product, type) + return .failure(AffiseSubscriptionError.userCancelled) + @unknown default: + onFailed(nil, product, type) + return .failure(AffiseSubscriptionError.purchaseFailed(nil)) + } + } catch { + onFailed(nil, product, type) + return .failure(error) + } + onFailed(nil, product, type) + return .failure(AffiseSubscriptionError.purchaseFailed(nil)) + } +} + +@available(iOS 15, *) +extension TransactionManagerSK2: TransactionManager { + + func purchase(_ product: AffiseProduct, _ type: AffiseProductType?, _ callback: @escaping AffiseResultCallback) { + let productId = product.productId + guard let skProduct = self.getProduct(productId) else { + callback(.failure(AffiseSubscriptionError.productNotFound([productId]))) + return + } + Task { + let result = await self.purchase(skProduct, type) + callback(result) + } + } +} diff --git a/AffiseModule/Subscription/Classes/utils/AffiseProductExt.swift b/AffiseModule/Subscription/Classes/utils/AffiseProductExt.swift index 30c8123..04ba669 100644 --- a/AffiseModule/Subscription/Classes/utils/AffiseProductExt.swift +++ b/AffiseModule/Subscription/Classes/utils/AffiseProductExt.swift @@ -8,4 +8,9 @@ public extension AffiseProduct { var skProduct: SKProduct? { self.productDetails as? SKProduct } + + @available(iOS 15, *) + var sk2Product: Product? { + self.productDetails as? Product + } } diff --git a/AffiseModule/Subscription/Classes/utils/AffisePurchasedInfoExt.swift b/AffiseModule/Subscription/Classes/utils/AffisePurchasedInfoExt.swift index 891a931..4ea38e1 100644 --- a/AffiseModule/Subscription/Classes/utils/AffisePurchasedInfoExt.swift +++ b/AffiseModule/Subscription/Classes/utils/AffisePurchasedInfoExt.swift @@ -18,4 +18,16 @@ public extension AffisePurchasedInfo { operationDate: transaction.transactionDate ) } + + + @available(iOS 15, *) + internal convenience init(_ transaction: Transaction, _ product: AffiseProduct?) { + self.init( + transaction, + product, + orderId: "\(transaction.id)", + originalOrderId: "\(transaction.originalID)", + operationDate: transaction.purchaseDate + ) + } } diff --git a/AffiseModule/Subscription/Classes/utils/StoreKit2Ext.swift b/AffiseModule/Subscription/Classes/utils/StoreKit2Ext.swift new file mode 100644 index 0000000..b584395 --- /dev/null +++ b/AffiseModule/Subscription/Classes/utils/StoreKit2Ext.swift @@ -0,0 +1,81 @@ +import Foundation +import StoreKit +import AffiseAttributionLib + + +@available(iOS 15, *) +internal extension Product { + + var toAffiseSubscriptionType: AffiseProductType? { + switch self.type { + case .consumable: + return AffiseProductType.CONSUMABLE + case .nonConsumable: + return AffiseProductType.NON_CONSUMABLE + case .autoRenewable: + return AffiseProductType.RENEWABLE_SUBSCRIPTION + case .nonRenewable: + return AffiseProductType.NON_RENEWABLE_SUBSCRIPTION + default: + return nil + } + } + + var toAffiseProductPrice: AffiseProductPrice { + return AffiseProductPrice( + value: self.price, + currencyCode: self.priceFormatStyle.currencyCode, + formattedPrice: self.displayPrice + ) + } + + var toAffiseProductSubscriptionDetail: AffiseProductSubscriptionDetail? { + return self.subscription?.toAffiseProductSubscriptionDetail() + } + + func toAffiseProduct(_ type: AffiseProductType? = nil) -> AffiseProduct { + self.price + return AffiseProduct( + productId: self.id, + title: self.displayName, + productDescription: self.description, + productType: self.toAffiseSubscriptionType ?? type, + price: self.toAffiseProductPrice, + subscription: self.toAffiseProductSubscriptionDetail, + productDetails: self + ) + } +} + + +@available(iOS 15, *) +internal extension Product.SubscriptionInfo { + + func toAffiseProductSubscriptionDetail() -> AffiseProductSubscriptionDetail? { + return AffiseProductSubscriptionDetail( + offerId: self.subscriptionGroupID, + timeUnit: self.subscriptionPeriod.unitType, + numberOfUnits: self.subscriptionPeriod.value + ) + } +} + + +@available(iOS 15, *) +internal extension Product.SubscriptionPeriod { + + var unitType: TimeUnitType { + switch self.unit { + case .day: + return .DAY + case .week: + return .WEEK + case .month: + return .MONTH + case .year: + return .YEAR + default: + return .DAY + } + } +} diff --git a/AffiseSKAdNetwork.podspec b/AffiseSKAdNetwork.podspec index 3b2715c..47f97d6 100644 --- a/AffiseSKAdNetwork.podspec +++ b/AffiseSKAdNetwork.podspec @@ -5,7 +5,7 @@ Pod::Spec.new do |spec| spec.name = "AffiseSKAdNetwork" - spec.version = ENV['LIB_VERSION'] || "1.6.40" + spec.version = ENV['LIB_VERSION'] || "1.6.41" spec.summary = "AffiseSKAdNetwork iOS library" spec.description = "Affise library for StoreKit Ad Network (SKAdNetwork)" spec.homepage = "https://github.com/affise/sdk-ios" diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ef352f..77041f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [1.6.41] - 2024-10-01 + +### Added + +- Module `Subscription` StoreKit2 support. + ## [1.6.40] - 2024-09-03 ### Fixed @@ -116,6 +122,7 @@ - Improve `AffiseModuleManager` - Update for demo app +[1.6.41]: https://github.com/affise/sdk-ios/compare/1.6.40...1.6.41 [1.6.40]: https://github.com/affise/sdk-ios/compare/1.6.39...1.6.40 [1.6.39]: https://github.com/affise/sdk-ios/compare/1.6.38...1.6.39 [1.6.38]: https://github.com/affise/sdk-ios/compare/1.6.37...1.6.38 diff --git a/README.md b/README.md index 02829c1..2d54c39 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,12 @@ | Pod | Version | | --------------------------- |:-------------------------:| -| `AffiseAttributionLib` | [`1.6.40`](https://github.com/CocoaPods/Specs/tree/master/Specs/a/9/3/AffiseAttributionLib) | -| `AffiseSKAdNetwork` | [`1.6.40`](https://github.com/CocoaPods/Specs/tree/master/Specs/3/6/f/AffiseSKAdNetwork) | -| `AffiseModule/Advertising` | [`1.6.40`](https://github.com/CocoaPods/Specs/tree/master/Specs/0/3/d/AffiseModule/) | -| `AffiseModule/Link` | [`1.6.40`](https://github.com/CocoaPods/Specs/tree/master/Specs/0/3/d/AffiseModule/) | -| `AffiseModule/Status` | [`1.6.40`](https://github.com/CocoaPods/Specs/tree/master/Specs/0/3/d/AffiseModule/) | -| `AffiseModule/Subscription` | [`1.6.40`](https://github.com/CocoaPods/Specs/tree/master/Specs/0/3/d/AffiseModule/) | +| `AffiseAttributionLib` | [`1.6.41`](https://github.com/CocoaPods/Specs/tree/master/Specs/a/9/3/AffiseAttributionLib) | +| `AffiseSKAdNetwork` | [`1.6.41`](https://github.com/CocoaPods/Specs/tree/master/Specs/3/6/f/AffiseSKAdNetwork) | +| `AffiseModule/Advertising` | [`1.6.41`](https://github.com/CocoaPods/Specs/tree/master/Specs/0/3/d/AffiseModule/) | +| `AffiseModule/Link` | [`1.6.41`](https://github.com/CocoaPods/Specs/tree/master/Specs/0/3/d/AffiseModule/) | +| `AffiseModule/Status` | [`1.6.41`](https://github.com/CocoaPods/Specs/tree/master/Specs/0/3/d/AffiseModule/) | +| `AffiseModule/Subscription` | [`1.6.41`](https://github.com/CocoaPods/Specs/tree/master/Specs/0/3/d/AffiseModule/) | - [Affise Attribution iOS Library](#affise-attribution-ios-library) - [Description](#description) @@ -85,24 +85,24 @@ To add the SDK using Cocoapods, specify the version you want to use in your Podf ```ruby # Affise SDK library -pod 'AffiseAttributionLib', '~> 1.6.40' +pod 'AffiseAttributionLib', '~> 1.6.41' # Affise modules -pod 'AffiseModule/Advertising', '~> 1.6.40' -pod 'AffiseModule/Link', '~> 1.6.40' -pod 'AffiseModule/Status', '~> 1.6.40' -pod 'AffiseModule/Subscription', '~> 1.6.40' +pod 'AffiseModule/Advertising', '~> 1.6.41' +pod 'AffiseModule/Link', '~> 1.6.41' +pod 'AffiseModule/Status', '~> 1.6.41' +pod 'AffiseModule/Subscription', '~> 1.6.41' ``` Get source directly from GitHub ```ruby # Affise SDK library -pod 'AffiseAttributionLib', :git => 'https://github.com/affise/sdk-ios.git', :tag => '1.6.40' +pod 'AffiseAttributionLib', :git => 'https://github.com/affise/sdk-ios.git', :tag => '1.6.41' # Affise modules -pod 'AffiseModule/Advertising', :git => 'https://github.com/affise/sdk-ios.git', :tag => '1.6.40' -pod 'AffiseModule/Link', :git => 'https://github.com/affise/sdk-ios.git', :tag => '1.6.40' -pod 'AffiseModule/Status', :git => 'https://github.com/affise/sdk-ios.git', :tag => '1.6.40' -pod 'AffiseModule/Subscription', :git => 'https://github.com/affise/sdk-ios.git', :tag => '1.6.40' +pod 'AffiseModule/Advertising', :git => 'https://github.com/affise/sdk-ios.git', :tag => '1.6.41' +pod 'AffiseModule/Link', :git => 'https://github.com/affise/sdk-ios.git', :tag => '1.6.41' +pod 'AffiseModule/Status', :git => 'https://github.com/affise/sdk-ios.git', :tag => '1.6.41' +pod 'AffiseModule/Subscription', :git => 'https://github.com/affise/sdk-ios.git', :tag => '1.6.41' ``` ### Integrate as Swift Package Manager @@ -204,10 +204,10 @@ Affise | Module | Version | Start | | -------------- |:------------------------------------------------------------------------------------:|----------| -| `Advertising` | [`1.6.40`](https://github.com/CocoaPods/Specs/tree/master/Specs/0/3/d/AffiseModule/) | `Manual` | -| `Link` | [`1.6.40`](https://github.com/CocoaPods/Specs/tree/master/Specs/0/3/d/AffiseModule/) | `Auto` | -| `Status` | [`1.6.40`](https://github.com/CocoaPods/Specs/tree/master/Specs/0/3/d/AffiseModule/) | `Auto` | -| `Subscription` | [`1.6.40`](https://github.com/CocoaPods/Specs/tree/master/Specs/0/3/d/AffiseModule/) | `Auto` | +| `Advertising` | [`1.6.41`](https://github.com/CocoaPods/Specs/tree/master/Specs/0/3/d/AffiseModule/) | `Manual` | +| `Link` | [`1.6.41`](https://github.com/CocoaPods/Specs/tree/master/Specs/0/3/d/AffiseModule/) | `Auto` | +| `Status` | [`1.6.41`](https://github.com/CocoaPods/Specs/tree/master/Specs/0/3/d/AffiseModule/) | `Auto` | +| `Subscription` | [`1.6.41`](https://github.com/CocoaPods/Specs/tree/master/Specs/0/3/d/AffiseModule/) | `Auto` | If module start type is `manual`, then call: @@ -311,14 +311,14 @@ To add the SDK using Cocoapods, specify the version you want to use in your Podf ```ruby # Wrapper for StoreKit Ad Network -pod 'AffiseSKAdNetwork', '~> 1.6.40' +pod 'AffiseSKAdNetwork', '~> 1.6.41' ``` Get source directly from GitHub ```ruby # Wrapper for StoreKit Ad Network -pod 'AffiseSKAdNetwork', :git => 'https://github.com/affise/sdk-ios.git', :tag => '1.6.40' +pod 'AffiseSKAdNetwork', :git => 'https://github.com/affise/sdk-ios.git', :tag => '1.6.41' ``` For `swift` use: