From b5b21ff9c7e36dd20fab1c4784242bcfefd7baf4 Mon Sep 17 00:00:00 2001 From: Sergey Korney Date: Mon, 1 Apr 2024 20:39:53 +0500 Subject: [PATCH] 1.6.28 --- AffiseAttributionLib.podspec | 2 +- .../Classes/events/EventName.swift | 1 + .../events/parameters/PredefinedString.swift | 6 + .../predefined/FailedPurchaseEvent.swift | 15 ++ .../internal/platform/InternalModules.swift | 6 + .../Classes/modules/AffiseModules.swift | 2 + .../modules/subscription/AffiseProduct.swift | 51 +++++ .../subscription/AffiseProductType.swift | 17 ++ .../subscription/AffiseProductsResult.swift | 16 ++ .../subscription/AffisePurchasedInfo.swift | 29 +++ .../modules/subscription/AffiseResult.swift | 5 + .../subscription/AffiseSubscriptionApi.swift | 5 + .../AffiseSubscriptionError.swift | 17 ++ .../modules/subscription/TimeUnitType.swift | 17 ++ .../init/AffSDKVersionProvider.swift | 2 +- AffiseInternal.podspec | 2 +- AffiseModule.podspec | 7 +- .../Subscription/Classes/AffiseExt.swift | 13 ++ .../Classes/SubscriptionModule.swift | 34 +++ .../Classes/store/StoreManager.swift | 17 ++ .../store/storekit/ProductManager.swift | 102 +++++++++ .../store/storekit/TransactionManager.swift | 203 ++++++++++++++++++ .../Classes/utils/AffiseProductExt.swift | 21 ++ .../utils/AffisePurchasedInfoExt.swift | 20 ++ .../Classes/utils/StoreKitExt.swift | 78 +++++++ AffiseSKAdNetwork.podspec | 2 +- Package.swift | 10 + README.md | 28 +-- example/app/Podfile | 2 +- example/app/app.xcodeproj/project.pbxproj | 6 + .../xcshareddata/xcschemes/app.xcscheme | 3 + example/app/app/AppDelegate.swift | 6 +- example/app/app/Base.lproj/Main.storyboard | 6 + example/app/app/StoreView.swift | 196 +++++++++++++++++ example/app/app/ViewController.swift | 43 +++- example/app/app/test.storekit | 190 ++++++++++++++++ 36 files changed, 1150 insertions(+), 30 deletions(-) create mode 100644 AffiseAttributionLib/Classes/events/predefined/FailedPurchaseEvent.swift create mode 100644 AffiseAttributionLib/Classes/internal/platform/InternalModules.swift create mode 100644 AffiseAttributionLib/Classes/modules/subscription/AffiseProduct.swift create mode 100644 AffiseAttributionLib/Classes/modules/subscription/AffiseProductType.swift create mode 100644 AffiseAttributionLib/Classes/modules/subscription/AffiseProductsResult.swift create mode 100644 AffiseAttributionLib/Classes/modules/subscription/AffisePurchasedInfo.swift create mode 100644 AffiseAttributionLib/Classes/modules/subscription/AffiseResult.swift create mode 100644 AffiseAttributionLib/Classes/modules/subscription/AffiseSubscriptionApi.swift create mode 100644 AffiseAttributionLib/Classes/modules/subscription/AffiseSubscriptionError.swift create mode 100644 AffiseAttributionLib/Classes/modules/subscription/TimeUnitType.swift create mode 100644 AffiseModule/Subscription/Classes/AffiseExt.swift create mode 100644 AffiseModule/Subscription/Classes/SubscriptionModule.swift create mode 100644 AffiseModule/Subscription/Classes/store/StoreManager.swift create mode 100644 AffiseModule/Subscription/Classes/store/storekit/ProductManager.swift create mode 100644 AffiseModule/Subscription/Classes/store/storekit/TransactionManager.swift create mode 100644 AffiseModule/Subscription/Classes/utils/AffiseProductExt.swift create mode 100644 AffiseModule/Subscription/Classes/utils/AffisePurchasedInfoExt.swift create mode 100644 AffiseModule/Subscription/Classes/utils/StoreKitExt.swift create mode 100644 example/app/app/StoreView.swift create mode 100644 example/app/app/test.storekit diff --git a/AffiseAttributionLib.podspec b/AffiseAttributionLib.podspec index aaee19e..6b22ead 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.27" + spec.version = ENV['LIB_VERSION'] || "1.6.28" 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/events/EventName.swift b/AffiseAttributionLib/Classes/events/EventName.swift index 513344d..922127d 100644 --- a/AffiseAttributionLib/Classes/events/EventName.swift +++ b/AffiseAttributionLib/Classes/events/EventName.swift @@ -38,6 +38,7 @@ public enum EventName: String { case ORDER_RETURN_REQUEST = "OrderReturnRequest" case ORDER_RETURN_REQUEST_CANCEL = "OrderReturnRequestCancel" case PURCHASE = "Purchase" + case FAILED_PURCHASE = "FailedPurchase" case RATE = "Rate" case RE_ENGAGE = "ReEngage" case RESERVE = "Reserve" diff --git a/AffiseAttributionLib/Classes/events/parameters/PredefinedString.swift b/AffiseAttributionLib/Classes/events/parameters/PredefinedString.swift index 980448d..3425ecf 100644 --- a/AffiseAttributionLib/Classes/events/parameters/PredefinedString.swift +++ b/AffiseAttributionLib/Classes/events/parameters/PredefinedString.swift @@ -70,6 +70,9 @@ public enum PredefinedString: Int { case NETWORK case UNIT case PLACEMENT + case PRODUCT_TYPE + case SUBSCRIPTION_TYPE + case ORIGINAL_ORDER_ID var enumValue: String { switch self { @@ -141,6 +144,9 @@ public enum PredefinedString: Int { case .NETWORK: return "network" case .UNIT: return "unit" case .PLACEMENT: return "placement" + case .PRODUCT_TYPE: return "product_type" + case .SUBSCRIPTION_TYPE: return "subscription_type" + case .ORIGINAL_ORDER_ID: return "original_order_id" } } } diff --git a/AffiseAttributionLib/Classes/events/predefined/FailedPurchaseEvent.swift b/AffiseAttributionLib/Classes/events/predefined/FailedPurchaseEvent.swift new file mode 100644 index 0000000..79282a6 --- /dev/null +++ b/AffiseAttributionLib/Classes/events/predefined/FailedPurchaseEvent.swift @@ -0,0 +1,15 @@ +import Foundation + +/** + * Event FailedPurchase use + * + * @property userData any custom data. + * @property timeStampMillis the timestamp event in milliseconds. + */ +@objc +public class FailedPurchaseEvent : NativeEvent { + + override public func getName() -> String { + return EventName.FAILED_PURCHASE.eventName + } +} diff --git a/AffiseAttributionLib/Classes/internal/platform/InternalModules.swift b/AffiseAttributionLib/Classes/internal/platform/InternalModules.swift new file mode 100644 index 0000000..ede08b6 --- /dev/null +++ b/AffiseAttributionLib/Classes/internal/platform/InternalModules.swift @@ -0,0 +1,6 @@ +public class InternalModules { + + public static func getModule(_ name: AffiseModules) -> AffiseModule? { + return Affise.getApi()?.moduleManager.getModule(name) + } +} diff --git a/AffiseAttributionLib/Classes/modules/AffiseModules.swift b/AffiseAttributionLib/Classes/modules/AffiseModules.swift index 19c4d61..6ee38a4 100644 --- a/AffiseAttributionLib/Classes/modules/AffiseModules.swift +++ b/AffiseAttributionLib/Classes/modules/AffiseModules.swift @@ -4,11 +4,13 @@ import Foundation public enum AffiseModules: Int { case Advertising case Status + case Subscription internal var enumValue: String { switch self { case .Advertising: return "Advertising" case .Status: return "Status" + case .Subscription: return "Subscription" } } } diff --git a/AffiseAttributionLib/Classes/modules/subscription/AffiseProduct.swift b/AffiseAttributionLib/Classes/modules/subscription/AffiseProduct.swift new file mode 100644 index 0000000..039ce64 --- /dev/null +++ b/AffiseAttributionLib/Classes/modules/subscription/AffiseProduct.swift @@ -0,0 +1,51 @@ +import Foundation + +@objc +public class AffiseProduct: NSObject { + + public internal(set) var type: AffiseProductType? + + public internal(set) var productId: String? + + public internal(set) var localizedTitle: String? + + public internal(set) var localizedDescription: String? + + public internal(set) var price: Decimal? + + public internal(set) var currencyCode: String? + + public internal(set) var currencySymbol: String? + + public internal(set) var regionCode: String? + + public internal(set) var priceLocale: Locale? + + public internal(set) var skData: Any? = nil + + public convenience init( + type: AffiseProductType?, + productId: String?, + localizedTitle: String?, + localizedDescription: String?, + price: Decimal?, + priceLocale: Locale?, + skData: Any? + ) { + self.init() + self.type = type + self.productId = productId + self.localizedTitle = localizedTitle + self.localizedDescription = localizedDescription + self.price = price + self.currencyCode = priceLocale?.currencyCode + self.currencySymbol = priceLocale?.currencySymbol + self.regionCode = priceLocale?.regionCode + self.priceLocale = priceLocale + self.skData = skData + } + + public override var description: String { + "AffiseProduct(productId=\"\(productId ?? "")\", localizedTitle=\"\(localizedTitle ?? "")\", price=\"\(price ?? 0)\", currencyCode=\"\(currencyCode ?? "")\", type=\"\(type?.enumValue ?? "")\")" + } +} diff --git a/AffiseAttributionLib/Classes/modules/subscription/AffiseProductType.swift b/AffiseAttributionLib/Classes/modules/subscription/AffiseProductType.swift new file mode 100644 index 0000000..6fc0176 --- /dev/null +++ b/AffiseAttributionLib/Classes/modules/subscription/AffiseProductType.swift @@ -0,0 +1,17 @@ +@objc +public enum AffiseProductType: Int { + + case CONSUMABLE + case NON_CONSUMABLE + case RENEWABLE_SUBSCRIPTION + case NON_RENEWABLE_SUBSCRIPTION + + public var enumValue: String { + switch self { + case .CONSUMABLE: return "consumable" + case .NON_CONSUMABLE: return "non_consumable" + case .RENEWABLE_SUBSCRIPTION: return "renewable_subscription" + case .NON_RENEWABLE_SUBSCRIPTION: return "non_renewable_subscription" + } + } +} diff --git a/AffiseAttributionLib/Classes/modules/subscription/AffiseProductsResult.swift b/AffiseAttributionLib/Classes/modules/subscription/AffiseProductsResult.swift new file mode 100644 index 0000000..a7f5c52 --- /dev/null +++ b/AffiseAttributionLib/Classes/modules/subscription/AffiseProductsResult.swift @@ -0,0 +1,16 @@ +@objc +public class AffiseProductsResult: NSObject { + + public let products: [String: AffiseProduct] + + public let invalidIds: [String] + + public init(products: [String: AffiseProduct], invalid: [String]) { + self.products = products + self.invalidIds = invalid + } + + public override var description: String { + "AffiseProductsResult(products=[\(products.keys.joined(separator: ", "))], invalidIds=[\(invalidIds.joined(separator: ", "))])" + } +} diff --git a/AffiseAttributionLib/Classes/modules/subscription/AffisePurchasedInfo.swift b/AffiseAttributionLib/Classes/modules/subscription/AffisePurchasedInfo.swift new file mode 100644 index 0000000..052365d --- /dev/null +++ b/AffiseAttributionLib/Classes/modules/subscription/AffisePurchasedInfo.swift @@ -0,0 +1,29 @@ +import Foundation + + +@objc +public class AffisePurchasedInfo: NSObject { + + public internal(set) var product: AffiseProduct? + + public internal(set) var operationDate: Date? + + public internal(set) var orderId: String? + + public internal(set) var originalOrderId: String? + + public internal(set) var skData: Any? = nil + + public convenience init(_ transaction: Any?, _ product: AffiseProduct?, orderId: String? = nil, originalOrderId: String?, operationDate: Date?) { + self.init() + self.skData = transaction + self.product = product + self.orderId = orderId + self.originalOrderId = originalOrderId + self.operationDate = operationDate + } + + public override var description: String { + "AffisePurchasedInfo(productId=\"\(product?.productId ?? "")\", orderId=\"\(orderId ?? "")\", originalOrderId=\"\(originalOrderId ?? "")\")" + } +} diff --git a/AffiseAttributionLib/Classes/modules/subscription/AffiseResult.swift b/AffiseAttributionLib/Classes/modules/subscription/AffiseResult.swift new file mode 100644 index 0000000..aa89a7c --- /dev/null +++ b/AffiseAttributionLib/Classes/modules/subscription/AffiseResult.swift @@ -0,0 +1,5 @@ +import Foundation + +public typealias AffiseResult = Swift.Result + +public typealias AffiseResultCallback = (AffiseResult) -> Void \ No newline at end of file diff --git a/AffiseAttributionLib/Classes/modules/subscription/AffiseSubscriptionApi.swift b/AffiseAttributionLib/Classes/modules/subscription/AffiseSubscriptionApi.swift new file mode 100644 index 0000000..7ab6a38 --- /dev/null +++ b/AffiseAttributionLib/Classes/modules/subscription/AffiseSubscriptionApi.swift @@ -0,0 +1,5 @@ +public protocol AffiseSubscriptionApi { + static func fetchProducts(_ productsIds: [String], _ callback: @escaping AffiseResultCallback) + + static func purchase(_ productId: String, _ type: AffiseProductType?, _ callback: @escaping AffiseResultCallback) +} diff --git a/AffiseAttributionLib/Classes/modules/subscription/AffiseSubscriptionError.swift b/AffiseAttributionLib/Classes/modules/subscription/AffiseSubscriptionError.swift new file mode 100644 index 0000000..244297a --- /dev/null +++ b/AffiseAttributionLib/Classes/modules/subscription/AffiseSubscriptionError.swift @@ -0,0 +1,17 @@ +import Foundation + +public enum AffiseSubscriptionError: Error { + case notInitialized + case productNotFound([String]) + case purchaseFailed(Error?) +} + +extension AffiseSubscriptionError: CustomStringConvertible { + public var description: String { + switch self { + 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)" + } + } +} diff --git a/AffiseAttributionLib/Classes/modules/subscription/TimeUnitType.swift b/AffiseAttributionLib/Classes/modules/subscription/TimeUnitType.swift new file mode 100644 index 0000000..ad2f429 --- /dev/null +++ b/AffiseAttributionLib/Classes/modules/subscription/TimeUnitType.swift @@ -0,0 +1,17 @@ +@objc +public enum TimeUnitType: Int { + + case DAY + case WEEK + case MONTH + case YEAR + + public var enumValue: String { + switch self { + case .DAY: return "day" + case .WEEK: return "week" + case .MONTH: return "month" + case .YEAR: return "year" + } + } +} \ No newline at end of file diff --git a/AffiseAttributionLib/Classes/parameters/providers/init/AffSDKVersionProvider.swift b/AffiseAttributionLib/Classes/parameters/providers/init/AffSDKVersionProvider.swift index 2489061..f1c0a72 100644 --- a/AffiseAttributionLib/Classes/parameters/providers/init/AffSDKVersionProvider.swift +++ b/AffiseAttributionLib/Classes/parameters/providers/init/AffSDKVersionProvider.swift @@ -6,7 +6,7 @@ import Foundation class AffSDKVersionProvider: StringPropertyProvider { override func provide() -> String? { - return "1.6.27" + return "1.6.28" } public override func getOrder() -> Float { diff --git a/AffiseInternal.podspec b/AffiseInternal.podspec index 44e32f1..21bae35 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.27" + spec.version = ENV['LIB_VERSION'] || "1.6.28" spec.summary = "Affise Internal library" spec.description = "Affise Internal wrapper library for crossplatform" spec.homepage = "https://github.com/affise/sdk-ios" diff --git a/AffiseModule.podspec b/AffiseModule.podspec index 40c745d..50caa49 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.27" + s.version = ENV["LIB_VERSION"] || "1.6.28" s.summary = "Affise Modules" s.description = "Affise module collection" s.homepage = "https://github.com/affise/sdk-ios" @@ -32,4 +32,9 @@ Pod::Spec.new do |s| s.subspec "Advertising" do |sub| sub.source_files = "AffiseModule/Advertising/Classes/**/*.{swift}" end + + s.subspec "Subscription" do |sub| + sub.source_files = "AffiseModule/Subscription/Classes/**/*.{swift}" + sub.framework = "StoreKit" + end end diff --git a/AffiseModule/Subscription/Classes/AffiseExt.swift b/AffiseModule/Subscription/Classes/AffiseExt.swift new file mode 100644 index 0000000..e588f7e --- /dev/null +++ b/AffiseModule/Subscription/Classes/AffiseExt.swift @@ -0,0 +1,13 @@ +import AffiseAttributionLib + + +extension Affise: AffiseSubscriptionApi { + + public static func fetchProducts(_ ids: [String], _ callback: @escaping AffiseResultCallback) { + SubscriptionModule.fetchProducts(ids, callback) + } + + public static func purchase(_ id: String, _ type: AffiseProductType? = nil, _ callback: @escaping AffiseResultCallback) { + SubscriptionModule.purchase(id, type, callback) + } +} diff --git a/AffiseModule/Subscription/Classes/SubscriptionModule.swift b/AffiseModule/Subscription/Classes/SubscriptionModule.swift new file mode 100644 index 0000000..5db3109 --- /dev/null +++ b/AffiseModule/Subscription/Classes/SubscriptionModule.swift @@ -0,0 +1,34 @@ +import AffiseAttributionLib +import Foundation +import UIKit + + +@objc(AffiseSubscriptionModule) +internal final class SubscriptionModule: AffiseModule { + + private(set) static var instance: SubscriptionModule? = nil + + lazy var storeManager: StoreManager = StoreManager() + + override public func start() { + SubscriptionModule.instance = self + } +} + + +extension SubscriptionModule: AffiseSubscriptionApi { + + public static func fetchProducts(_ productsIds: [String], _ callback: @escaping AffiseResultCallback) { + guard let module = instance else { + return callback(.failure(AffiseSubscriptionError.notInitialized)) + } + module.storeManager.fetchProducts(productsIds, callback) + } + + public static func purchase(_ productId: String, _ type: AffiseProductType? = nil, _ callback: @escaping AffiseResultCallback) { + guard let module = instance else { + return callback(.failure(AffiseSubscriptionError.notInitialized)) + } + module.storeManager.purchase(productId, type, callback) + } +} diff --git a/AffiseModule/Subscription/Classes/store/StoreManager.swift b/AffiseModule/Subscription/Classes/store/StoreManager.swift new file mode 100644 index 0000000..67af124 --- /dev/null +++ b/AffiseModule/Subscription/Classes/store/StoreManager.swift @@ -0,0 +1,17 @@ +import AffiseAttributionLib + + +internal class StoreManager: NSObject { + + lazy var productManager: ProductManager = ProductManager() + lazy var transactionManager: TransactionManager = TransactionManager(productManager: productManager) + + func fetchProducts(_ productsIds: [String], _ callback: @escaping AffiseResultCallback) { + productManager.fetchProducts(productsIds, callback) + } + + func purchase(_ productId: String, _ type: AffiseProductType?, _ callback: @escaping AffiseResultCallback) { + transactionManager.purchase(productId, type, callback) + } +} + diff --git a/AffiseModule/Subscription/Classes/store/storekit/ProductManager.swift b/AffiseModule/Subscription/Classes/store/storekit/ProductManager.swift new file mode 100644 index 0000000..f733c2f --- /dev/null +++ b/AffiseModule/Subscription/Classes/store/storekit/ProductManager.swift @@ -0,0 +1,102 @@ +import StoreKit +import AffiseAttributionLib + + +internal class ProductManager: NSObject { + + private let queue = DispatchQueue(label: "Affise.ProductFetcher") + + private(set) var products: [String: SKProduct] = [:] + + 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)) + request.delegate = self + requests[request] = ids + request.start() + } +} + + +extension ProductManager: SKProductsRequestDelegate { + + func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { + queue.async { [weak self] in + guard let self = self else { return } + + guard let (ids, callbacks) = self.getIdsCallbacks(request) as? ([String], [AffiseResultCallback]) else { + return + } + + let invalid: [String] = response.invalidProductIdentifiers + + if !response.products.isEmpty { + self.products = response.products.reduce(into: [:]) { dict, product in + dict[product.productIdentifier] = product + } + + let products: [String:AffiseProduct] = self.products.reduce(into: [:]) { (dict, item) in + dict[item.key] = AffiseProduct(item.value) + } + + for callback in callbacks { + callback(.success(AffiseProductsResult(products: products, invalid: invalid))) + } + } else { + for callback in callbacks { + callback(.failure(AffiseSubscriptionError.productNotFound(ids))) + } + } + } + } + + func request(_ request: SKRequest, didFailWithError error: Error) { + defer { request.cancel() } + queue.async { [weak self] in + guard let self = self else { return } + + guard let (ids, callbacks) = self.getIdsCallbacks(request) as? ([String], [AffiseResultCallback]) else { + return + } + + for callback in callbacks { + callback(.failure(error)) + } + } + } + + func requestDidFinish(_ request: SKRequest) { + request.cancel() + } + + private func getIdsCallbacks(_ request: SKRequest) -> ([String]?, [AffiseResultCallback]?) { + guard let ids = self.requests[request] else { + return (nil, nil) + } + + self.requests.removeValue(forKey: request) + + guard let handlers = self.callbacks.removeValue(forKey: ids) else { + return (nil, nil) + } + + return (ids, handlers) + } +} diff --git a/AffiseModule/Subscription/Classes/store/storekit/TransactionManager.swift b/AffiseModule/Subscription/Classes/store/storekit/TransactionManager.swift new file mode 100644 index 0000000..00c04f2 --- /dev/null +++ b/AffiseModule/Subscription/Classes/store/storekit/TransactionManager.swift @@ -0,0 +1,203 @@ +import StoreKit +import AffiseAttributionLib + +internal class TransactionManager: NSObject { + + private let queue = DispatchQueue(label: "Affise.TransactionManager") + + private var productManager: ProductManager? = nil + + private var transactionProducts: [String: (SKProduct, AffiseProductType?)] = [:] + + private var callbacks: [String: ([AffiseResultCallback], AffiseProductType?)] = [:] + + init(productManager: ProductManager) { + super.init() + SKPaymentQueue.default().add(self) + self.productManager = productManager + } + + func purchase(_ productId: String, _ type: AffiseProductType?, _ callback: @escaping AffiseResultCallback) { + queue.async { [weak self] in + guard let self = self else { return } + + 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) + } + } + + func failedTransaction(_ transaction: SKPaymentTransaction) { + queue.async { [weak self] in + guard let self = self else { return } + SKPaymentQueue.default().finishTransaction(transaction) + + let productId = transaction.payment.productIdentifier + let (product, type) = self.removeTransactionByProduct(productId) + + self.result(productId, .failure(AffiseSubscriptionError.purchaseFailed(transaction.error))) + + guard let product = product else { + return + } + + self.createEvent(transaction, product: product, type: type, failed: true)? + .send() + } + } + + func purchasedTransaction(_ transaction: SKPaymentTransaction) { + queue.async { [weak self] in + guard let self = self else { return } + SKPaymentQueue.default().finishTransaction(transaction) + + let productId = transaction.payment.productIdentifier + let (product, type) = self.removeTransactionByProduct(productId) + + self.result(productId, .success(AffisePurchasedInfo(transaction, AffiseProduct(product, type)))) + + guard let product = product else { + return + } + + self.createEvent(transaction, product: product, type: type, failed: false)? + .send() + } + } + + func removeTransactionByProduct(_ productId: String) -> (SKProduct?, AffiseProductType?) { + var product: SKProduct? = nil + var type: AffiseProductType? = nil + + if let (transactionProduct, transactionProductType) = transactionProducts.removeValue(forKey: productId) { + product = transactionProduct + type = transactionProductType + } + + return (product, type) + } + + func result(_ productId: String, _ result: AffiseResult) { + queue.async { [weak self] in + guard let self = self else { return } + + guard let (callbacks, _) = self.callbacks.removeValue(forKey: productId) else { + return + } + + for callback in callbacks { + callback(result) + } + } + } + + func canMakePurchases() -> Bool { return SKPaymentQueue.canMakePayments() } + + func createEvent(_ transaction: SKPaymentTransaction, product: SKProduct, type: AffiseProductType?, failed: Bool) -> Event? { + var event: Event? + var eventType: AffiseProductType = type ?? .CONSUMABLE + if product.isSubscription { + eventType = .RENEWABLE_SUBSCRIPTION + } + + let orderId: String? = transaction.transactionIdentifier + let originalOrderId: String? = transaction.original?.transactionIdentifier + + switch eventType { + case .CONSUMABLE, .NON_CONSUMABLE: + if failed { + event = FailedPurchaseEvent() + } else { + event = PurchaseEvent() + } + event? + .addPredefinedParameter(.PRODUCT_ID, string: product.productIdentifier) + .addPredefinedParameter(.PRODUCT_TYPE, string: eventType.enumValue) + + case .RENEWABLE_SUBSCRIPTION, .NON_RENEWABLE_SUBSCRIPTION: + if failed { + event = FailedSubscriptionEvent() + } else { + if (originalOrderId ?? "").isEmpty { + event = SubscribeEvent() + } else { + event = RenewedSubscriptionEvent() + } + } + + var timeUnit: String = "" + var numberOfUnits: Int64 = 0 + + if #available(iOS 11.2, *) { + timeUnit = product.subscriptionPeriod?.unitType.enumValue ?? "" + numberOfUnits = Int64(product.subscriptionPeriod?.numberOfUnits ?? 0 ) + } + + event? + .addPredefinedParameter(.SUBSCRIPTION_ID, string: product.productIdentifier) + .addPredefinedParameter(.SUBSCRIPTION_TYPE, string: eventType.enumValue) + .addPredefinedParameter(.UNIT, string: timeUnit) + .addPredefinedParameter(.QUANTITY, long: numberOfUnits) + + default: break + } + + event? + .addPredefinedParameter(.ORDER_ID, string: orderId ?? "") + .addPredefinedParameter(.ORIGINAL_ORDER_ID, string: originalOrderId ?? "") + .addPredefinedParameter(.CURRENCY, string: product.priceLocale.currencyCode ?? "") + .addPredefinedParameter(.PRICE, float: product.price.floatValue ?? 0.0) + + return event + } +} + + +extension TransactionManager: SKPaymentTransactionObserver { + func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { + for transaction in transactions { + switch transaction.transactionState { + case .purchased: + purchasedTransaction(transaction) + + case .failed: + failedTransaction(transaction) + + case .restored: + SKPaymentQueue.default().finishTransaction(transaction) + + default: break + } + } + } + + func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) { + } + + func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) { + } + + func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment, for product: SKProduct) -> Bool { + if canMakePurchases() { + let payment = SKPayment(product: product) + SKPaymentQueue.default().add(self) + SKPaymentQueue.default().add(payment) + + return true + } else { + return false + } + } +} diff --git a/AffiseModule/Subscription/Classes/utils/AffiseProductExt.swift b/AffiseModule/Subscription/Classes/utils/AffiseProductExt.swift new file mode 100644 index 0000000..4361230 --- /dev/null +++ b/AffiseModule/Subscription/Classes/utils/AffiseProductExt.swift @@ -0,0 +1,21 @@ +import AffiseAttributionLib +import StoreKit + +public extension AffiseProduct { + + var skProduct: SKProduct? { + self.skData as? SKProduct + } + + internal convenience init(_ product: SKProduct?, _ type: AffiseProductType? = nil) { + self.init( + type: type, + productId: product?.productIdentifier, + localizedTitle: product?.localizedTitle, + localizedDescription: product?.localizedDescription, + price: product?.price.decimalValue ?? 0, + priceLocale: product?.priceLocale, + skData: product + ) + } +} diff --git a/AffiseModule/Subscription/Classes/utils/AffisePurchasedInfoExt.swift b/AffiseModule/Subscription/Classes/utils/AffisePurchasedInfoExt.swift new file mode 100644 index 0000000..56ad434 --- /dev/null +++ b/AffiseModule/Subscription/Classes/utils/AffisePurchasedInfoExt.swift @@ -0,0 +1,20 @@ +import StoreKit +import AffiseAttributionLib + + +public extension AffisePurchasedInfo { + + var skTransaction: SKPaymentTransaction? { + self.skData as? SKPaymentTransaction + } + + internal convenience init(_ transaction: SKPaymentTransaction, _ product: AffiseProduct?) { + self.init( + transaction, + product, + orderId: transaction.transactionIdentifier, + originalOrderId: transaction.original?.transactionIdentifier, + operationDate: transaction.transactionDate + ) + } +} diff --git a/AffiseModule/Subscription/Classes/utils/StoreKitExt.swift b/AffiseModule/Subscription/Classes/utils/StoreKitExt.swift new file mode 100644 index 0000000..9903e53 --- /dev/null +++ b/AffiseModule/Subscription/Classes/utils/StoreKitExt.swift @@ -0,0 +1,78 @@ +import Foundation +import StoreKit +import AffiseAttributionLib + + +extension Date { + var toTimestamp: Int64 { + Int64(self.timeIntervalSince1970 * 1000) + } +} + +extension SKPaymentTransaction { + var toTimestamp: Int64 { + if let transactionDate = self.transactionDate { + return transactionDate.toTimestamp + } else { + return Date().toTimestamp + } + } +} + +extension SKProduct { + var isSubscription: Bool { + if #available(iOS 11.2, *) { + if self.subscriptionPeriod != nil { + return true + } + } else if #available(iOS 12.0, *) { + if self.subscriptionGroupIdentifier != nil { + return true + } + } + + return false + } + + func endTimestap(start: Date) -> Int64 { + if #available(iOS 11.2, *) { + return self.subscriptionPeriod?.add(to: start).toTimestamp ?? 0 + } else { + return 0 + } + } +} + +@available(iOS 11.2, *) +extension SKProductSubscriptionPeriod { + + func add(to: Date) -> Date { + return Calendar.current.date(byAdding: self.calendarUnit, value: self.numberOfUnits, to: to) ?? to + } + + var calendarUnit: Calendar.Component { + switch self.unit { + case .day: + return .day + case .week: + return .weekOfYear + case .month: + return .month + case .year: + return .year + } + } + + var unitType: TimeUnitType { + switch self.unit { + case .day: + return .DAY + case .week: + return .WEEK + case .month: + return .MONTH + case .year: + return .YEAR + } + } +} diff --git a/AffiseSKAdNetwork.podspec b/AffiseSKAdNetwork.podspec index 521fe27..c274336 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.27" + spec.version = ENV['LIB_VERSION'] || "1.6.28" 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/Package.swift b/Package.swift index 89f1c32..e35f4f8 100644 --- a/Package.swift +++ b/Package.swift @@ -12,6 +12,7 @@ let package = Package( .library(name: "AffiseAttributionLib", targets: ["AffiseAttributionLib"]), .library(name: "AffiseModuleAdvertising", targets: ["AffiseModuleAdvertising"]), .library(name: "AffiseModuleStatus", targets: ["AffiseModuleStatus"]), + .library(name: "AffiseSubscriptionModule", targets: ["AffiseSubscriptionModule"]), .library(name: "AffiseSKAdNetwork", targets: ["AffiseSKAdNetwork", "AffiseInternalWrapperObjC"]), .library(name: "AffiseInternal", targets: ["AffiseInternal"]), ], @@ -36,6 +37,15 @@ let package = Package( path: "AffiseModule/Status", sources: [ "Classes" ] ), + .target( + name: "AffiseSubscriptionModule", + dependencies: ["AffiseAttributionLib"], + path: "AffiseModule/Subscription", + sources: [ "Classes" ], + linkerSettings: [ + .linkedFramework("StoreKit"), + ] + ), .target( name: "AffiseSKAdNetwork", dependencies: ["AffiseInternalWrapperObjC"], diff --git a/README.md b/README.md index 8860931..e64f7a1 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,10 @@ | Pod | Version | | ---- |:-------:| -| `AffiseAttributionLib` | [`1.6.27`](https://github.com/CocoaPods/Specs/tree/master/Specs/a/9/3/AffiseAttributionLib) | -| `AffiseSKAdNetwork` | [`1.6.27`](https://github.com/CocoaPods/Specs/tree/master/Specs/3/6/f/AffiseSKAdNetwork) | -| `AffiseModule/Advertising` | [`1.6.27`](https://github.com/CocoaPods/Specs/tree/master/Specs/0/3/d/AffiseModule/) | -| `AffiseModule/Status` | [`1.6.27`](https://github.com/CocoaPods/Specs/tree/master/Specs/0/3/d/AffiseModule/) | +| `AffiseAttributionLib` | [`1.6.28`](https://github.com/CocoaPods/Specs/tree/master/Specs/a/9/3/AffiseAttributionLib) | +| `AffiseSKAdNetwork` | [`1.6.28`](https://github.com/CocoaPods/Specs/tree/master/Specs/3/6/f/AffiseSKAdNetwork) | +| `AffiseModule/Advertising` | [`1.6.28`](https://github.com/CocoaPods/Specs/tree/master/Specs/0/3/d/AffiseModule/) | +| `AffiseModule/Status` | [`1.6.28`](https://github.com/CocoaPods/Specs/tree/master/Specs/0/3/d/AffiseModule/) | - [Affise Attribution iOS Library](#affise-attribution-ios-library) - [Description](#description) @@ -74,20 +74,20 @@ To add the SDK using Cocoapods, specify the version you want to use in your Podf ```ruby # Affise SDK library -pod 'AffiseAttributionLib', '~> 1.6.27' +pod 'AffiseAttributionLib', '~> 1.6.28' # Affise modules -pod 'AffiseModule/Advertising', '~> 1.6.27' -pod 'AffiseModule/Status', '~> 1.6.27' +pod 'AffiseModule/Advertising', '~> 1.6.28' +pod 'AffiseModule/Status', '~> 1.6.28' ``` Get source directly from GitHub ```ruby # Affise SDK library -pod 'AffiseAttributionLib', :git => 'https://github.com/affise/sdk-ios.git', :tag => '1.6.27' +pod 'AffiseAttributionLib', :git => 'https://github.com/affise/sdk-ios.git', :tag => '1.6.28' # Affise modules -pod 'AffiseModule/Advertising', :git => 'https://github.com/affise/sdk-ios.git', :tag => '1.6.27' -pod 'AffiseModule/Status', :git => 'https://github.com/affise/sdk-ios.git', :tag => '1.6.27' +pod 'AffiseModule/Advertising', :git => 'https://github.com/affise/sdk-ios.git', :tag => '1.6.28' +pod 'AffiseModule/Status', :git => 'https://github.com/affise/sdk-ios.git', :tag => '1.6.28' ``` ### Integrate as Swift Package Manager @@ -189,8 +189,8 @@ Affise | Module | Version | Start | | ------------- |:------------------------------------------------------------------------------------:|----------| -| `Advertising` | [`1.6.27`](https://github.com/CocoaPods/Specs/tree/master/Specs/0/3/d/AffiseModule/) | `Manual` | -| `Status` | [`1.6.27`](https://github.com/CocoaPods/Specs/tree/master/Specs/0/3/d/AffiseModule/) | `Auto` | +| `Advertising` | [`1.6.28`](https://github.com/CocoaPods/Specs/tree/master/Specs/0/3/d/AffiseModule/) | `Manual` | +| `Status` | [`1.6.28`](https://github.com/CocoaPods/Specs/tree/master/Specs/0/3/d/AffiseModule/) | `Auto` | If module start type is `manual`, then call: @@ -242,14 +242,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.27' +pod 'AffiseSKAdNetwork', '~> 1.6.28' ``` Get source directly from GitHub ```ruby # Wrapper for StoreKit Ad Network -pod 'AffiseSKAdNetwork', :git => 'https://github.com/affise/sdk-ios.git', :tag => '1.6.27' +pod 'AffiseSKAdNetwork', :git => 'https://github.com/affise/sdk-ios.git', :tag => '1.6.28' ``` For `swift` use: diff --git a/example/app/Podfile b/example/app/Podfile index 827a72b..d8c357a 100644 --- a/example/app/Podfile +++ b/example/app/Podfile @@ -8,6 +8,6 @@ workspace 'app' target 'app' do pod 'AffiseAttributionLib', :path => '../../' pod 'AffiseSKAdNetwork', :path => '../../' - pod 'AffiseInternal', :path => '../../' + # pod 'AffiseInternal', :path => '../../' pod 'AffiseModule', :path => '../../' end diff --git a/example/app/app.xcodeproj/project.pbxproj b/example/app/app.xcodeproj/project.pbxproj index ff0b2e7..cb4c1b7 100644 --- a/example/app/app.xcodeproj/project.pbxproj +++ b/example/app/app.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 10C07E3828B737D400A903FD /* EventsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10C07E3728B737D400A903FD /* EventsFactory.swift */; }; 10C07E3B28B7380C00A903FD /* DefaultEventsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10C07E3A28B7380C00A903FD /* DefaultEventsFactory.swift */; }; 26CDECE9BB0CA0706ECEC29A /* Pods_app.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 337C855EA0BA3B031C2BE0DF /* Pods_app.framework */; }; + 2B3251CC2BB7C6E30045C92D /* StoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B3251CB2BB7C6E30045C92D /* StoreView.swift */; }; 7C139994287F2FC000C28C32 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C139993287F2FC000C28C32 /* AppDelegate.swift */; }; 7C139998287F2FC000C28C32 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C139997287F2FC000C28C32 /* ViewController.swift */; }; 7C13999B287F2FC000C28C32 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7C139999287F2FC000C28C32 /* Main.storyboard */; }; @@ -45,6 +46,8 @@ 10EE600929E52EF400174ED3 /* AffiseAttributionLib.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AffiseAttributionLib.framework; path = "../../../../Desktop/AffiseAttributionLib.xcframework/ios-arm64_armv7/AffiseAttributionLib.framework"; sourceTree = ""; }; 10EE600D29E52F1F00174ED3 /* AffiseAttributionLib.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AffiseAttributionLib.framework; path = "../../../../Desktop/AffiseAttributionLib.xcframework/ios-arm64_i386_x86_64-simulator/AffiseAttributionLib.framework"; sourceTree = ""; }; 10EE601129E52F4900174ED3 /* AffiseAttributionLib.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = AffiseAttributionLib.framework; sourceTree = ""; }; + 2B09694C2BAB348D005123F4 /* test.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = test.storekit; sourceTree = ""; }; + 2B3251CB2BB7C6E30045C92D /* StoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreView.swift; sourceTree = ""; }; 337C855EA0BA3B031C2BE0DF /* Pods_app.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_app.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7C139990287F2FC000C28C32 /* app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = app.app; sourceTree = BUILT_PRODUCTS_DIR; }; 7C139993287F2FC000C28C32 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -128,7 +131,9 @@ children = ( 10C07E3628B737C400A903FD /* factories */, 7C139993287F2FC000C28C32 /* AppDelegate.swift */, + 2B09694C2BAB348D005123F4 /* test.storekit */, 7C139997287F2FC000C28C32 /* ViewController.swift */, + 2B3251CB2BB7C6E30045C92D /* StoreView.swift */, 7C139999287F2FC000C28C32 /* Main.storyboard */, 7C13999C287F2FC100C28C32 /* Assets.xcassets */, 7C13999E287F2FC100C28C32 /* LaunchScreen.storyboard */, @@ -340,6 +345,7 @@ 7C139998287F2FC000C28C32 /* ViewController.swift in Sources */, 10C07E3828B737D400A903FD /* EventsFactory.swift in Sources */, 7C139994287F2FC000C28C32 /* AppDelegate.swift in Sources */, + 2B3251CC2BB7C6E30045C92D /* StoreView.swift in Sources */, 10C07E3B28B7380C00A903FD /* DefaultEventsFactory.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/example/app/app.xcodeproj/xcshareddata/xcschemes/app.xcscheme b/example/app/app.xcodeproj/xcshareddata/xcschemes/app.xcscheme index 84e2ab0..dbafa9a 100644 --- a/example/app/app.xcodeproj/xcshareddata/xcschemes/app.xcscheme +++ b/example/app/app.xcodeproj/xcshareddata/xcschemes/app.xcscheme @@ -50,6 +50,9 @@ ReferencedContainer = "container:app.xcodeproj"> + + + @@ -70,6 +71,10 @@ + @@ -90,6 +95,7 @@ + diff --git a/example/app/app/StoreView.swift b/example/app/app/StoreView.swift new file mode 100644 index 0000000..8e8665a --- /dev/null +++ b/example/app/app/StoreView.swift @@ -0,0 +1,196 @@ +import SwiftUI +import AffiseAttributionLib +#if canImport(AffiseModule) +import AffiseModule +#elseif canImport(AffiseSubscriptionModule) +import AffiseSubscriptionModule +#endif +import StoreKit + + +struct Product { + static let all: [AffiseProductType: [String]] = [ + AffiseProductType.CONSUMABLE : [ + "com.testapp.consum_1", + "com.testapp.consum_2", + "com.testapp.invalid" + ], + AffiseProductType.NON_CONSUMABLE : [ + "com.testapp.non_consum_1", + "com.testapp.non_consum_2" + ], + AffiseProductType.NON_RENEWABLE_SUBSCRIPTION : [ + "com.testapp.subs_1", + "com.testapp.subs_2" + ], + AffiseProductType.RENEWABLE_SUBSCRIPTION : [ + "com.testapp.auto_subs_week", + "com.testapp.auto_subs_month", + "com.testapp.auto_subs_year" + ], + ] + + static var allIds: [String] { + all.values.flatMap { $0 } + } + + static func getType(_ id: String) -> AffiseProductType? { + all.first { $0.value.contains(id) }?.key + } +} + +func titleByType(_ type: AffiseProductType) -> String { + switch type { + case .CONSUMABLE: return "CONSUMABLE" + case .NON_CONSUMABLE: return "NON CONSUMABLE " + case .RENEWABLE_SUBSCRIPTION: return "Auto renew SUBSCRIPTION" + case .NON_RENEWABLE_SUBSCRIPTION: return "SUBSCRIPTION" + } +} + +@available(iOS 13.0, *) +struct StoreView: View { + + @State var products: [AffiseProduct] = [] + + func typeMatch(_ product: AffiseProduct, _ type: AffiseProductType) -> Bool { + guard let id = product.productId else { return false } + return Product.all[type]?.contains(id) ?? false + } + + var body: some View { + VStack { + ForEach(Array(Product.all.keys.sorted { $0.rawValue < $1.rawValue }), id: \.self) { type in + List { + Section { + ForEach(products, id: \.productId) { product in + if typeMatch(product, type) { + ProductRowView(product: product) + } + } + } header: { + Text(titleByType(type)) + .font(.headline) + } + } + .frame(maxWidth:.infinity) + .edgesIgnoringSafeArea(.all) + .listStyle(.automatic) + } + + Button("Fetch Products") { + initProducts() + } + } + .onAppear { + initProducts() + } + } + + func initProducts() { + #if canImport(AffiseModule) || canImport(AffiseSubscriptionModule) + Affise.fetchProducts(Product.allIds) { result in + switch result { + case .failure(let error): + print("\(error)") + case .success(let result): + products = result.products + .map { $0.value } + .sorted { $0.price ?? 0 < $1.price ?? 0 } + + print("invalid ids: [\(result.invalidIds.joined(separator: ", "))]") + } + } + #endif + } +} + + +@available(iOS 13.0, *) +struct ProductRowView: View { + + var product: AffiseProduct + + let currencyFormatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.numberStyle = .currency + return formatter + }() + + func price(_ amount: Decimal?, _ local: Locale?) -> String? { + guard let amount = amount else { + return nil + } + + currencyFormatter.locale = local + return currencyFormatter.string(from: amount as NSNumber) + } + + var body: some View { + HStack { + VStack(alignment: .leading) { + Text(product.localizedTitle ?? "-") + .font(.body) + Text(product.localizedDescription ?? "") + .font(.footnote) + .foregroundColor(Color.gray) + } + .frame(maxWidth:.infinity, alignment: .leading) + + Text( + price(product.price, product.priceLocale) ?? "-" + ) + + if let productId = product.productId { + Button("Buy") { + purchase(productId) + } + } + } + .padding([.leading, .trailing], 0) + .frame(maxWidth:.infinity, alignment: .leading) + .listRowBackground(Color.gray.opacity(0.2)) + } + + func purchase(_ id: String) { + #if canImport(AffiseModule) || canImport(AffiseSubscriptionModule) + Affise.purchase(id, Product.getType(id)) { result in + switch result { + case .failure(let error): + print("\(error)") + case .success(let purchasedInfo): + print("\(purchasedInfo)") + } + } + #endif + } +} + +#if targetEnvironment(simulator) +@available(iOS 13.0, *) +struct StoreView_Previews: PreviewProvider { + + static let products: [AffiseProduct] = [ + AffiseProduct("Preview product 1", 0.01, "Test"), + AffiseProduct("Preview product 2", 0.02, "Test"), + ] + + static var previews: some View { + StoreView(products: products) + } +} + +extension AffiseProduct { + convenience init(_ title: String, _ price: Decimal, _ description: String? = nil) { + self.init( + type: nil, + productId: "test", + localizedTitle: title, + localizedDescription: description, + price: price, + priceLocale: Locale.current, + skData: nil + ) + } +} +#endif diff --git a/example/app/app/ViewController.swift b/example/app/app/ViewController.swift index 4dd8b5e..213ca08 100644 --- a/example/app/app/ViewController.swift +++ b/example/app/app/ViewController.swift @@ -1,6 +1,8 @@ import UIKit import WebKit import AffiseAttributionLib +import SwiftUI + class ViewController: UIViewController, WKScriptMessageHandler { func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { @@ -11,6 +13,7 @@ class ViewController: UIViewController, WKScriptMessageHandler { @IBOutlet weak var stackView: UIStackView! @IBOutlet weak var webViewWrapper: UIView! @IBOutlet weak var eventsWrapper: UIScrollView! + @IBOutlet weak var storeWrapper: UIView! var webView: WKWebView? @@ -57,13 +60,29 @@ class ViewController: UIViewController, WKScriptMessageHandler { webView.loadFileURL(indexURL, allowingReadAccessTo: indexURL) webViewWrapper.addSubview(webView) - webView.translatesAutoresizingMaskIntoConstraints = false - webView.topAnchor.constraint(equalTo: webViewWrapper.topAnchor, constant: 0).isActive = true - webView.leftAnchor.constraint(equalTo: webViewWrapper.leftAnchor, constant: 16).isActive = true - webView.bottomAnchor.constraint(equalTo: webViewWrapper.bottomAnchor, constant: 0).isActive = true - webView.rightAnchor.constraint(equalTo: webViewWrapper.rightAnchor, constant: -16).isActive = true + webView.fillAnchor(webViewWrapper) Affise.registerWebView(webView) } + + storeUi(storeWrapper) + } + + func storeUi(_ root: UIView) { + if #available(iOS 13.0, *) { + if let view = UIHostingController(rootView: StoreView()).view { + root.addSubview(view) + view.fillAnchor(root) + } + } else { + let label = UILabel() + label.text = "No SwiftUi View" + label.textColor = UIColor.red + label.textAlignment = .center + label.adjustsFontSizeToFitWidth = true + + root.addSubview(label) + label.fillAnchor(root) + } } @objc @@ -75,8 +94,18 @@ class ViewController: UIViewController, WKScriptMessageHandler { } @IBAction func didValueChangedControl(_ sender: UISegmentedControl) { - eventsWrapper.isHidden = sender.selectedSegmentIndex != 0 - webViewWrapper.isHidden = sender.selectedSegmentIndex == 0 + eventsWrapper.isHidden = sender.selectedSegmentIndex != 0 + webViewWrapper.isHidden = sender.selectedSegmentIndex != 1 + storeWrapper.isHidden = sender.selectedSegmentIndex != 2 } } +extension UIView { + func fillAnchor(_ root: UIView) { + self.translatesAutoresizingMaskIntoConstraints = false + self.topAnchor.constraint(equalTo: root.topAnchor, constant: 0).isActive = true + self.leftAnchor.constraint(equalTo: root.leftAnchor, constant: 16).isActive = true + self.bottomAnchor.constraint(equalTo: root.bottomAnchor, constant: 0).isActive = true + self.rightAnchor.constraint(equalTo: root.rightAnchor, constant: -16).isActive = true + } +} diff --git a/example/app/app/test.storekit b/example/app/app/test.storekit new file mode 100644 index 0000000..0bc9796 --- /dev/null +++ b/example/app/app/test.storekit @@ -0,0 +1,190 @@ +{ + "identifier" : "8C82C8A9", + "nonRenewingSubscriptions" : [ + { + "displayPrice" : "0.31", + "familyShareable" : false, + "internalID" : "8522A32A", + "localizations" : [ + { + "description" : "description", + "displayName" : "Subscription 1", + "locale" : "en_US" + } + ], + "productID" : "com.testapp.subs_1", + "referenceName" : "Subscription 1", + "type" : "NonRenewingSubscription" + }, + { + "displayPrice" : "0.32", + "familyShareable" : false, + "internalID" : "C65DE88D", + "localizations" : [ + { + "description" : "description", + "displayName" : "Subscription 2", + "locale" : "en_US" + } + ], + "productID" : "com.testapp.subs_2", + "referenceName" : "Subscription 2", + "type" : "NonRenewingSubscription" + } + ], + "products" : [ + { + "displayPrice" : "0.11", + "familyShareable" : false, + "internalID" : "6424A388", + "localizations" : [ + { + "description" : "description", + "displayName" : "Consumable 1", + "locale" : "en_US" + } + ], + "productID" : "com.testapp.consum_1", + "referenceName" : "Consumable 1", + "type" : "Consumable" + }, + { + "displayPrice" : "0.21", + "familyShareable" : false, + "internalID" : "24EE3BDF", + "localizations" : [ + { + "description" : "description", + "displayName" : "Non consumable 1", + "locale" : "en_US" + } + ], + "productID" : "com.testapp.non_consum_1", + "referenceName" : "Non consumable 1", + "type" : "NonConsumable" + }, + { + "displayPrice" : "0.12", + "familyShareable" : false, + "internalID" : "D44949CF", + "localizations" : [ + { + "description" : "description", + "displayName" : "Consumable 2", + "locale" : "en_US" + } + ], + "productID" : "com.testapp.consum_2", + "referenceName" : "Consumable 2", + "type" : "Consumable" + }, + { + "displayPrice" : "0.22", + "familyShareable" : false, + "internalID" : "C6DD400F", + "localizations" : [ + { + "description" : "description", + "displayName" : "Non consumable 2", + "locale" : "en_US" + } + ], + "productID" : "com.testapp.non_consum_2", + "referenceName" : "Non consumable 2", + "type" : "NonConsumable" + } + ], + "settings" : { + + }, + "subscriptionGroups" : [ + { + "id" : "BEE43702", + "localizations" : [ + + ], + "name" : "Renewable", + "subscriptions" : [ + { + "adHocOffers" : [ + + ], + "codeOffers" : [ + + ], + "displayPrice" : "0.43", + "familyShareable" : false, + "groupNumber" : 1, + "internalID" : "E92AF38A", + "introductoryOffer" : null, + "localizations" : [ + { + "description" : "description", + "displayName" : "Auto subs year", + "locale" : "en_US" + } + ], + "productID" : "com.testapp.auto_subs_year", + "recurringSubscriptionPeriod" : "P1Y", + "referenceName" : "Auto subs year", + "subscriptionGroupID" : "BEE43702", + "type" : "RecurringSubscription" + }, + { + "adHocOffers" : [ + + ], + "codeOffers" : [ + + ], + "displayPrice" : "0.41", + "familyShareable" : false, + "groupNumber" : 1, + "internalID" : "D9EFF0F0", + "introductoryOffer" : null, + "localizations" : [ + { + "description" : "description", + "displayName" : "Auto subs week", + "locale" : "en_US" + } + ], + "productID" : "com.testapp.auto_subs_week", + "recurringSubscriptionPeriod" : "P1W", + "referenceName" : "Auto subs week", + "subscriptionGroupID" : "BEE43702", + "type" : "RecurringSubscription" + }, + { + "adHocOffers" : [ + + ], + "codeOffers" : [ + + ], + "displayPrice" : "0.42", + "familyShareable" : false, + "groupNumber" : 1, + "internalID" : "E0DF6C12", + "introductoryOffer" : null, + "localizations" : [ + { + "description" : "description", + "displayName" : "Auto subs month", + "locale" : "en_US" + } + ], + "productID" : "com.testapp.auto_subs_month", + "recurringSubscriptionPeriod" : "P1M", + "referenceName" : "Auto subs month", + "subscriptionGroupID" : "BEE43702", + "type" : "RecurringSubscription" + } + ] + } + ], + "version" : { + "major" : 1, + "minor" : 2 + } +}