From e0190bc836884ff917f4a34b59c0bb08053a25bd Mon Sep 17 00:00:00 2001 From: Calvin Collins Date: Mon, 26 Jul 2021 12:19:10 -0700 Subject: [PATCH 1/6] added subscription delegate --- LUX.podspec | 8 +++ StoreKit/LUXSubscriptionDelegate.swift | 90 ++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 StoreKit/LUXSubscriptionDelegate.swift diff --git a/LUX.podspec b/LUX.podspec index 2c90988..5d87b07 100644 --- a/LUX.podspec +++ b/LUX.podspec @@ -133,4 +133,12 @@ Pod::Spec.new do |s| sp.dependency 'LUX/BaseSearch' end + s.subspec 'StoreKit' do |sp| + sp.source_files = 'LUX/Classes/StoreKit/**/*.swift' + sp.resources = 'LUX/Classes/StoreKit/**/*.xib' + sp.ios.deployment_target = '13.0' + sp.dependency 'FlexDataSource' + sp.dependency 'LithoOperators' + end + end diff --git a/StoreKit/LUXSubscriptionDelegate.swift b/StoreKit/LUXSubscriptionDelegate.swift new file mode 100644 index 0000000..11ec3db --- /dev/null +++ b/StoreKit/LUXSubscriptionDelegate.swift @@ -0,0 +1,90 @@ +// +// LUXSubscriptionDelegate.swift +// LUX +// +// Created by Calvin Collins on 7/26/21. +// + +import Foundation +import StoreKit +import Slippers +import Combine +import Prelude +import LithoOperators +import FunNet + + + +public class LUXSubscriptionDelegate: NSObject, SKProductsRequestDelegate, SKPaymentTransactionObserver, Refreshable { + + public var cancelBag: Set = [] + @Published var products: [SKProduct] = [] + public var productsCall: CombineNetCall? + + public var onFailed: ((SKPaymentTransaction) -> Void)? + public var onPurchased: ((SKPaymentTransaction) -> Void)? + public var onPurchasing: ((SKPaymentTransaction) -> Void)? + public var onDeferred: ((SKPaymentTransaction) -> Void)? + public var onRestored: ((SKPaymentTransaction) -> Void)? + + public var onRefresh: (() -> Void)? + + public init(onFailed: ((SKPaymentTransaction) -> Void)? = nil, onPurchased: ((SKPaymentTransaction) -> Void)? = nil, onPurchasing: ((SKPaymentTransaction) -> Void)? = nil, onDeferred: ((SKPaymentTransaction) -> Void)? = nil, onRestored: ((SKPaymentTransaction) -> Void)? = nil) { + super.init() + self.onFailed = onFailed + self.onPurchased = onPurchased + self.onPurchasing = onPurchasing + self.onDeferred = onDeferred + self.onRestored = onRestored + } + + public func refresh() { + onRefresh?() + } + + open func fetchProducts(withIdentifiers identifiers: [String]) { + let request = productIdsToRequest(identifiers) + request.delegate = self + request.start() + onRefresh = identifiers *> fetchProducts + } + + open func fetchProducts(from call: CombineNetCall, unwrapper: @escaping (T) -> [String]) { + self.productsCall = call + let productPub = unwrappedModelPublisher(from: call.publisher.$data.eraseToAnyPublisher(), unwrapper) + productPub.sink(receiveValue: fetchProducts).store(in: &cancelBag) + } + + open func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { + DispatchQueue.main.async { + self.products = response.products + } + } + + open func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { + transactions.forEach(handleUpdatedTransactions(onFailed: onFailed, onPurchased: onPurchased, onDeferred: onDeferred, onPurchasing: onPurchasing, onRestored: onRestored)) + } +} + +public let productIdsToRequest: ([String]) -> SKProductsRequest = Set.init >>> SKProductsRequest.init + +public func handleUpdatedTransactions(onFailed: ((SKPaymentTransaction) -> Void)?, onPurchased: ((SKPaymentTransaction) -> Void)?, onDeferred: ((SKPaymentTransaction) -> Void)?, onPurchasing: ((SKPaymentTransaction) -> Void)?, onRestored: ((SKPaymentTransaction) -> Void)?) -> (SKPaymentTransaction) -> Void { + return { transaction in + switch transaction.transactionState { + case .failed: + SKPaymentQueue.default().finishTransaction(transaction) + onFailed?(transaction) + case .purchased: + SKPaymentQueue.default().finishTransaction(transaction) + onPurchased?(transaction) + case .purchasing: + onPurchasing?(transaction) + case .deferred: + onDeferred?(transaction) + case .restored: + onRestored?(transaction) + @unknown default: + break + } + } +} From 2e6de57a246dcdb68d6c87e47f2b28eb09f91fb7 Mon Sep 17 00:00:00 2001 From: Calvin Collins Date: Tue, 27 Jul 2021 10:11:29 -0700 Subject: [PATCH 2/6] added playground and vc --- Example/LUX.xcodeproj/project.pbxproj | 10 ++ .../xcschemes/LUX-Example.xcscheme | 3 + Example/LUX/Configuration.storekit | 93 +++++++++++++++++++ .../Contents.swift | 5 +- .../Subscriptions.playground/Contents.swift | 36 +++++++ .../contents.xcplayground | 4 + .../timeline.xctimeline | 0 LUX.podspec | 1 + StoreKit/LUXSubscriptionDelegate.swift | 6 +- StoreKit/LUXSubscriptionTableViewCell.swift | 13 +++ StoreKit/LUXSubscriptionTableViewCell.xib | 56 +++++++++++ StoreKit/LUXSubscriptionViewController.swift | 62 +++++++++++++ StoreKit/LUXSubscriptionViewController.xib | 53 +++++++++++ 13 files changed, 338 insertions(+), 4 deletions(-) create mode 100644 Example/LUX/Configuration.storekit create mode 100644 Example/LUX/Subscriptions.playground/Contents.swift create mode 100644 Example/LUX/Subscriptions.playground/contents.xcplayground rename Example/LUX/{SectionTableViewModel.playground => Subscriptions.playground}/timeline.xctimeline (100%) create mode 100644 StoreKit/LUXSubscriptionTableViewCell.swift create mode 100644 StoreKit/LUXSubscriptionTableViewCell.xib create mode 100644 StoreKit/LUXSubscriptionViewController.swift create mode 100644 StoreKit/LUXSubscriptionViewController.xib diff --git a/Example/LUX.xcodeproj/project.pbxproj b/Example/LUX.xcodeproj/project.pbxproj index 0a854a0..fd51726 100644 --- a/Example/LUX.xcodeproj/project.pbxproj +++ b/Example/LUX.xcodeproj/project.pbxproj @@ -38,6 +38,8 @@ 6410ADE325993FD100022E72 /* UIColor+HexTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6410ADE225993FD100022E72 /* UIColor+HexTests.swift */; }; 6410ADE7259942A500022E72 /* CollectionViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6410ADE6259942A500022E72 /* CollectionViewModelTests.swift */; }; 6410ADEB2599480300022E72 /* LUXSessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6410ADEA2599480300022E72 /* LUXSessionTests.swift */; }; + 64133B2526AF6E3C003B61DC /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 64133B2426AF6E3C003B61DC /* StoreKit.framework */; }; + 64133B2826AF8BC8003B61DC /* Configuration.storekit in Resources */ = {isa = PBXBuildFile; fileRef = 64133B2626AF6E68003B61DC /* Configuration.storekit */; }; 6418E0D825AA77480050BEFB /* LUXTableViewCellTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6418E0D725AA77480050BEFB /* LUXTableViewCellTests.swift */; }; 6418E0DC25AA7ED40050BEFB /* LoginViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6418E0DB25AA7ED40050BEFB /* LoginViewControllerTests.swift */; }; 6418E0E025AA9E530050BEFB /* LUXCollectionViewCellTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6418E0DF25AA9E530050BEFB /* LUXCollectionViewCellTests.swift */; }; @@ -107,6 +109,9 @@ 6410ADE225993FD100022E72 /* UIColor+HexTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+HexTests.swift"; sourceTree = ""; }; 6410ADE6259942A500022E72 /* CollectionViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewModelTests.swift; sourceTree = ""; }; 6410ADEA2599480300022E72 /* LUXSessionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LUXSessionTests.swift; sourceTree = ""; }; + 64133B2426AF6E3C003B61DC /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; + 64133B2626AF6E68003B61DC /* Configuration.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = Configuration.storekit; sourceTree = ""; }; + 64133B2726AF6F5F003B61DC /* Subscriptions.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = Subscriptions.playground; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 6418E0D725AA77480050BEFB /* LUXTableViewCellTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LUXTableViewCellTests.swift; sourceTree = ""; }; 6418E0DB25AA7ED40050BEFB /* LoginViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewControllerTests.swift; sourceTree = ""; }; 6418E0DF25AA9E530050BEFB /* LUXCollectionViewCellTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LUXCollectionViewCellTests.swift; sourceTree = ""; }; @@ -132,6 +137,7 @@ buildActionMask = 2147483647; files = ( B5DDDA9015D5B0752B631707 /* Pods_LUX_Example.framework in Frameworks */, + 64133B2526AF6E3C003B61DC /* StoreKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -149,6 +155,7 @@ 1D9211F776B1E433C8B86795 /* Frameworks */ = { isa = PBXGroup; children = ( + 64133B2426AF6E3C003B61DC /* StoreKit.framework */, B42B4BEA58B69C8BD8FE4014 /* Pods_LUX_Example.framework */, 84AB7549E70A7D5F45143895 /* Pods_LUX_Tests.framework */, ); @@ -180,6 +187,7 @@ 607FACD21AFB9204008FA782 /* Example for LUX */ = { isa = PBXGroup; children = ( + 64133B2726AF6F5F003B61DC /* Subscriptions.playground */, 64E57AAF261CD70300FF91C8 /* MultiSizeCollectionVIewLayout.playground */, 49CBFADB2540F8250007D7E6 /* CollectionViewModel.playground */, 491D44E2244A29F1009D6A3A /* SectionTableViewModel.playground */, @@ -195,6 +203,7 @@ 607FACDC1AFB9204008FA782 /* Images.xcassets */, 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */, 607FACD31AFB9204008FA782 /* Supporting Files */, + 64133B2626AF6E68003B61DC /* Configuration.storekit */, ); name = "Example for LUX"; path = LUX; @@ -369,6 +378,7 @@ buildActionMask = 2147483647; files = ( 49D3C363239C414700D59DF0 /* config.yml in Resources */, + 64133B2826AF8BC8003B61DC /* Configuration.storekit in Resources */, 607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */, 607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */, 607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */, diff --git a/Example/LUX.xcodeproj/xcshareddata/xcschemes/LUX-Example.xcscheme b/Example/LUX.xcodeproj/xcshareddata/xcschemes/LUX-Example.xcscheme index a2c9f47..afd946c 100644 --- a/Example/LUX.xcodeproj/xcshareddata/xcschemes/LUX-Example.xcscheme +++ b/Example/LUX.xcodeproj/xcshareddata/xcschemes/LUX-Example.xcscheme @@ -94,6 +94,9 @@ ReferencedContainer = "container:LUX.xcodeproj"> + + Void = { emperor, cell in } //linking models to views -let emperorToItemCreator: (@escaping (Emperor) -> Void) -> (Emperor) -> FlexDataSourceItem = { onTap in emperorConfigurator >||> (onTap >|||> LUXTappableModelItem.init) } +let emperorToItemCreator: (@escaping (Emperor) -> Void) -> (Emperor) -> FlexDataSourceItem = { onTap in emperorConfigurator -*> (onTap --*> LUXTappableModelItem.init) } func reignToSection(_ emperorToItem: @escaping (Emperor) -> FlexDataSourceItem) -> (Reign) -> FlexDataSourceSection { return { let section = FlexDataSourceSection() @@ -92,7 +93,7 @@ let cycleSignal: AnyPublisher = modelPublisher(from: dataSignal) let cancel = cycleSignal.sink { vc.title = "\($0.ordinal ?? 0)th Cycle" } let refreshManager = LUXRefreshableNetworkCallManager(call) -let vm = LUXSectionsTableViewModel(refreshManager, modelsSignal.map(reignToSection(emperorToItemCreator(onTap)) >||> map).eraseToAnyPublisher()) +let vm = LUXSectionsTableViewModel(refreshManager, modelsSignal.map(reignToSection(emperorToItemCreator(onTap)) -*> map).eraseToAnyPublisher()) let cancel3 = dataSignal.sink { _ in vm.endRefreshing() } vm.tableDelegate = FUITableViewDelegate(onSelect: (vm.dataSource as! FlexDataSource).tappableOnSelect) diff --git a/Example/LUX/Subscriptions.playground/Contents.swift b/Example/LUX/Subscriptions.playground/Contents.swift new file mode 100644 index 0000000..5fa0e48 --- /dev/null +++ b/Example/LUX/Subscriptions.playground/Contents.swift @@ -0,0 +1,36 @@ +import UIKit +import LUX +import PlaygroundVCHelpers +import FunNet +import PlaygroundSupport +import Slippers +import LithoOperators +import Prelude +import fuikit +import StoreKit + +let vc = LUXSubscriptionViewController.makeFromXIB(name: "LUXSubscriptionViewController", bundle: Bundle(for: LUXSubscriptionViewController.self)) + +struct StoreKitIdentifiers: Codable { + var identifiers: [String] +} + +let call = CombineNetCall(configuration: ServerConfiguration(host: "lithobyte.co", apiRoute: "api/v1"), Endpoint()) +call.firingFunc = { call in + call.publisher.data = JsonProvider.encode(StoreKitIdentifiers(identifiers: ["com.LUX.monthly", "com.LUX.biyearly", "com.LUX.yearly"])) +} + +let delegate = LUXSubscriptionDelegate() + +vc.onViewDidLoad = { (vc: FUITableViewViewController) in + delegate.fetchProducts(from: call, unwrapper: ^\StoreKitIdentifiers.identifiers) +} +let vm = subscriptionViewModel(onTap: { print($0.localizedTitle) }, delegate: delegate) +vc.onViewDidAppear = { (vc: FUITableViewViewController, animated: Bool) in + vm.tableView = vc.tableView + vm.tableView?.reloadData() + vm.refresh() +} + +PlaygroundPage.current.liveView = vc +PlaygroundPage.current.needsIndefiniteExecution = true diff --git a/Example/LUX/Subscriptions.playground/contents.xcplayground b/Example/LUX/Subscriptions.playground/contents.xcplayground new file mode 100644 index 0000000..a751024 --- /dev/null +++ b/Example/LUX/Subscriptions.playground/contents.xcplayground @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Example/LUX/SectionTableViewModel.playground/timeline.xctimeline b/Example/LUX/Subscriptions.playground/timeline.xctimeline similarity index 100% rename from Example/LUX/SectionTableViewModel.playground/timeline.xctimeline rename to Example/LUX/Subscriptions.playground/timeline.xctimeline diff --git a/LUX.podspec b/LUX.podspec index 5d87b07..0328b79 100644 --- a/LUX.podspec +++ b/LUX.podspec @@ -139,6 +139,7 @@ Pod::Spec.new do |s| sp.ios.deployment_target = '13.0' sp.dependency 'FlexDataSource' sp.dependency 'LithoOperators' + sp.dependency 'PlaygroundVCHelpers' end end diff --git a/StoreKit/LUXSubscriptionDelegate.swift b/StoreKit/LUXSubscriptionDelegate.swift index 11ec3db..402f30d 100644 --- a/StoreKit/LUXSubscriptionDelegate.swift +++ b/StoreKit/LUXSubscriptionDelegate.swift @@ -13,8 +13,6 @@ import Prelude import LithoOperators import FunNet - - public class LUXSubscriptionDelegate: NSObject, SKProductsRequestDelegate, SKPaymentTransactionObserver, Refreshable { public var cancelBag: Set = [] @@ -36,6 +34,7 @@ public class LUXSubscriptionDelegate: NSObject, SKProductsRequestDelegate, SKPay self.onPurchasing = onPurchasing self.onDeferred = onDeferred self.onRestored = onRestored + SKPaymentQueue.default().add(self) } public func refresh() { @@ -43,6 +42,7 @@ public class LUXSubscriptionDelegate: NSObject, SKProductsRequestDelegate, SKPay } open func fetchProducts(withIdentifiers identifiers: [String]) { + print(identifiers.count) let request = productIdsToRequest(identifiers) request.delegate = self request.start() @@ -53,9 +53,11 @@ public class LUXSubscriptionDelegate: NSObject, SKProductsRequestDelegate, SKPay self.productsCall = call let productPub = unwrappedModelPublisher(from: call.publisher.$data.eraseToAnyPublisher(), unwrapper) productPub.sink(receiveValue: fetchProducts).store(in: &cancelBag) + call.fire() } open func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { + print("received response") DispatchQueue.main.async { self.products = response.products } diff --git a/StoreKit/LUXSubscriptionTableViewCell.swift b/StoreKit/LUXSubscriptionTableViewCell.swift new file mode 100644 index 0000000..c1db7ec --- /dev/null +++ b/StoreKit/LUXSubscriptionTableViewCell.swift @@ -0,0 +1,13 @@ +// +// SubscriptionTableViewCell.swift +// LUX +// +// Created by Calvin Collins on 7/26/21. +// + +import UIKit + +public class LUXSubscriptionTableViewCell: UITableViewCell { + @IBOutlet weak var subscriptionNameLabel: UILabel! + @IBOutlet weak var subscriptionDescriptionLabel: UILabel! +} diff --git a/StoreKit/LUXSubscriptionTableViewCell.xib b/StoreKit/LUXSubscriptionTableViewCell.xib new file mode 100644 index 0000000..dafa58d --- /dev/null +++ b/StoreKit/LUXSubscriptionTableViewCell.xib @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/StoreKit/LUXSubscriptionViewController.swift b/StoreKit/LUXSubscriptionViewController.swift new file mode 100644 index 0000000..46a1070 --- /dev/null +++ b/StoreKit/LUXSubscriptionViewController.swift @@ -0,0 +1,62 @@ +// +// SubscriptionViewController.swift +// LUX +// +// Created by Calvin Collins on 7/26/21. +// + +import UIKit +import fuikit +import StoreKit +import Combine +import LithoOperators + + +public func subscriptionViewModel(configureCell: @escaping (SKProduct, C) -> Void = configureSubscriptionCell, onTap: @escaping (SKProduct) -> Void = productToPayment >?> addPayment, delegate: LUXSubscriptionDelegate = LUXSubscriptionDelegate()) -> LUXItemsTableViewModel { + let modelToItem = tappableModelItem(configureCell, onTap: onTap) -*> map + return LUXItemsTableViewModel(delegate, itemsPublisher: delegate.$products.map(modelToItem).eraseToAnyPublisher()) +} + +public class LUXSubscriptionViewController: FUITableViewViewController { + @IBOutlet weak var termsButton: UIButton! + + open var subscriptionsDelegate = LUXSubscriptionDelegate() + + public var onTermsPressed: (() -> Void)? + + @IBAction func termsPressed(_ sender: UIButton!) { + onTermsPressed?() + } +} + +public func productToPayment(product: SKProduct) -> (SKPayment?) { + return SKPaymentQueue.canMakePayments() ? SKPayment.init(product: product) : nil +} +public let addPayment: (SKPayment) -> Void = SKPaymentQueue.default().add + +public func configureSubscriptionCell(product: SKProduct, cell: LUXSubscriptionTableViewCell) { + cell.subscriptionNameLabel.text = "\(product.localizedTitle)" + cell.subscriptionDescriptionLabel.text = "\(product.localizedDescription)" +} + +public func productToPriceString(_ product: SKProduct) -> String { + let currency = product.priceLocale.currencySymbol ?? "" + let price = product.price + let period = product.subscriptionPeriod?.numberOfUnits + var unit: String? + switch product.subscriptionPeriod?.unit { + case .month: + if (period != nil && period != 1) { + unit = "\(period!) Months" + } else { + unit = "Month" + } + case .year: + unit = "Year" + case .none: + unit = "" + default: + unit = "" + } + return "\(currency)\(price)/\(unit ?? "")" +} diff --git a/StoreKit/LUXSubscriptionViewController.xib b/StoreKit/LUXSubscriptionViewController.xib new file mode 100644 index 0000000..311fbc7 --- /dev/null +++ b/StoreKit/LUXSubscriptionViewController.xib @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From b7474d2df05a765e72419e3604bf98c25764df9b Mon Sep 17 00:00:00 2001 From: Calvin Collins Date: Tue, 27 Jul 2021 10:24:55 -0700 Subject: [PATCH 3/6] added vc functions in --- Example/Podfile.lock | 8 +++++++- .../Classes/StoreKit}/LUXSubscriptionDelegate.swift | 0 .../StoreKit}/LUXSubscriptionTableViewCell.swift | 0 .../StoreKit}/LUXSubscriptionTableViewCell.xib | 0 .../StoreKit}/LUXSubscriptionViewController.swift | 12 ++++++++++++ .../StoreKit}/LUXSubscriptionViewController.xib | 0 6 files changed, 19 insertions(+), 1 deletion(-) rename {StoreKit => LUX/Classes/StoreKit}/LUXSubscriptionDelegate.swift (100%) rename {StoreKit => LUX/Classes/StoreKit}/LUXSubscriptionTableViewCell.swift (100%) rename {StoreKit => LUX/Classes/StoreKit}/LUXSubscriptionTableViewCell.xib (100%) rename {StoreKit => LUX/Classes/StoreKit}/LUXSubscriptionViewController.swift (72%) rename {StoreKit => LUX/Classes/StoreKit}/LUXSubscriptionViewController.xib (100%) diff --git a/Example/Podfile.lock b/Example/Podfile.lock index 9a2f348..200e765 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -34,6 +34,7 @@ PODS: - LUX/CollectionViews (= 0.2.20) - LUX/Networking (= 0.2.20) - LUX/Search (= 0.2.20) + - LUX/StoreKit (= 0.2.20) - LUX/TableViews (= 0.2.20) - LUX/Utilities (= 0.2.20) - LUX/AppOpenFlow (0.2.20): @@ -83,6 +84,11 @@ PODS: - LithoUtils/Core - LUX/BaseSearch - LUX/TableViews + - LUX/StoreKit (0.2.20): + - FlexDataSource + - LithoOperators + - LithoUtils/Core + - PlaygroundVCHelpers - LUX/TableViews (0.2.20): - LithoUtils/Core - LUX/BaseTableViews @@ -161,7 +167,7 @@ SPEC CHECKSUMS: FunNet: e66d2df77a5663556970a1914c5911ecde2842ef LithoOperators: 8fc0c6a49e34a8d1ca01b777844f58e36c062d9f LithoUtils: 672d313a7fede3968e22f082b293cbc01ebcb7f0 - LUX: 98b3495fc34d7fdb4ec47b4ad5a9d69e5838343d + LUX: 801e4c9f28b4989704d3fc2d741093799f0813a3 PlaygroundVCHelpers: c7cc8994d2851ebd1590217101b4c6888d1c9cc8 Prelude: fe4cc0fd961d34edf48fe6b04d05c863449efb0a SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d diff --git a/StoreKit/LUXSubscriptionDelegate.swift b/LUX/Classes/StoreKit/LUXSubscriptionDelegate.swift similarity index 100% rename from StoreKit/LUXSubscriptionDelegate.swift rename to LUX/Classes/StoreKit/LUXSubscriptionDelegate.swift diff --git a/StoreKit/LUXSubscriptionTableViewCell.swift b/LUX/Classes/StoreKit/LUXSubscriptionTableViewCell.swift similarity index 100% rename from StoreKit/LUXSubscriptionTableViewCell.swift rename to LUX/Classes/StoreKit/LUXSubscriptionTableViewCell.swift diff --git a/StoreKit/LUXSubscriptionTableViewCell.xib b/LUX/Classes/StoreKit/LUXSubscriptionTableViewCell.xib similarity index 100% rename from StoreKit/LUXSubscriptionTableViewCell.xib rename to LUX/Classes/StoreKit/LUXSubscriptionTableViewCell.xib diff --git a/StoreKit/LUXSubscriptionViewController.swift b/LUX/Classes/StoreKit/LUXSubscriptionViewController.swift similarity index 72% rename from StoreKit/LUXSubscriptionViewController.swift rename to LUX/Classes/StoreKit/LUXSubscriptionViewController.swift index 46a1070..41b7fb2 100644 --- a/StoreKit/LUXSubscriptionViewController.swift +++ b/LUX/Classes/StoreKit/LUXSubscriptionViewController.swift @@ -10,7 +10,19 @@ import fuikit import StoreKit import Combine import LithoOperators +import PlaygroundVCHelpers +public func subscriptionViewController(styleVC: @escaping (LUXSubscriptionViewController) -> Void, configureCell: @escaping (SKProduct, C) -> Void = configureSubscriptionCell, onTap: @escaping (SKProduct) -> Void = productToPayment >?> addPayment, termsPressed: (() -> Void)? = nil, delegate: LUXSubscriptionDelegate = LUXSubscriptionDelegate()) -> LUXSubscriptionViewController{ + let vc = LUXSubscriptionViewController.makeFromXIB() + let vm = subscriptionViewModel(configureCell: configureCell, onTap: onTap, delegate: delegate) + vc.onViewDidLoad = { + vm.tableView = $0.tableView + vm.tableView?.reloadData() + vm.refresh() + } + vc.onTermsPressed = termsPressed + return vc +} public func subscriptionViewModel(configureCell: @escaping (SKProduct, C) -> Void = configureSubscriptionCell, onTap: @escaping (SKProduct) -> Void = productToPayment >?> addPayment, delegate: LUXSubscriptionDelegate = LUXSubscriptionDelegate()) -> LUXItemsTableViewModel { let modelToItem = tappableModelItem(configureCell, onTap: onTap) -*> map diff --git a/StoreKit/LUXSubscriptionViewController.xib b/LUX/Classes/StoreKit/LUXSubscriptionViewController.xib similarity index 100% rename from StoreKit/LUXSubscriptionViewController.xib rename to LUX/Classes/StoreKit/LUXSubscriptionViewController.xib From bd215c78ca1ebca4ae2ad50f424cca9ba06af831 Mon Sep 17 00:00:00 2001 From: Calvin Collins Date: Mon, 2 Aug 2021 15:13:40 -0700 Subject: [PATCH 4/6] super subclass hierarchy --- .../StoreKit/LUXSubscriptionDelegate.swift | 76 +++++++++++++------ 1 file changed, 53 insertions(+), 23 deletions(-) diff --git a/LUX/Classes/StoreKit/LUXSubscriptionDelegate.swift b/LUX/Classes/StoreKit/LUXSubscriptionDelegate.swift index 402f30d..c112496 100644 --- a/LUX/Classes/StoreKit/LUXSubscriptionDelegate.swift +++ b/LUX/Classes/StoreKit/LUXSubscriptionDelegate.swift @@ -13,11 +13,8 @@ import Prelude import LithoOperators import FunNet -public class LUXSubscriptionDelegate: NSObject, SKProductsRequestDelegate, SKPaymentTransactionObserver, Refreshable { - - public var cancelBag: Set = [] - @Published var products: [SKProduct] = [] - public var productsCall: CombineNetCall? +public class LUXSubscriptionDelegate: NSObject, SKProductsRequestDelegate, SKPaymentTransactionObserver { + public var onReceiveProducts: (([SKProduct]) -> Void)? public var onFailed: ((SKPaymentTransaction) -> Void)? public var onPurchased: ((SKPaymentTransaction) -> Void)? @@ -25,46 +22,81 @@ public class LUXSubscriptionDelegate: NSObject, SKProductsRequestDelegate, SKPay public var onDeferred: ((SKPaymentTransaction) -> Void)? public var onRestored: ((SKPaymentTransaction) -> Void)? - public var onRefresh: (() -> Void)? - - public init(onFailed: ((SKPaymentTransaction) -> Void)? = nil, onPurchased: ((SKPaymentTransaction) -> Void)? = nil, onPurchasing: ((SKPaymentTransaction) -> Void)? = nil, onDeferred: ((SKPaymentTransaction) -> Void)? = nil, onRestored: ((SKPaymentTransaction) -> Void)? = nil) { + public init(onFailed: ((SKPaymentTransaction) -> Void)? = nil, onPurchased: ((SKPaymentTransaction) -> Void)? = nil, onPurchasing: ((SKPaymentTransaction) -> Void)? = nil, onDeferred: ((SKPaymentTransaction) -> Void)? = nil, onRestored: ((SKPaymentTransaction) -> Void)? = nil, onReceiveProducts: (([SKProduct]) -> Void)?) { super.init() self.onFailed = onFailed self.onPurchased = onPurchased self.onPurchasing = onPurchasing self.onDeferred = onDeferred self.onRestored = onRestored + self.onReceiveProducts = onReceiveProducts SKPaymentQueue.default().add(self) } + open func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { + DispatchQueue.main.async { [weak self] in + self?.onReceiveProducts?(response.products) + } + } + + open func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { + transactions.forEach(handleUpdatedTransactions(onFailed: onFailed, onPurchased: onPurchased, onDeferred: onDeferred, onPurchasing: onPurchasing, onRestored: onRestored)) + } +} + +public class LUXIdSubscriptionDelegate: LUXSubscriptionDelegate, Refreshable { + var onRefresh: (() -> Void)? + public func refresh() { onRefresh?() } + public override init(onFailed: ((SKPaymentTransaction) -> Void)? = SKPaymentQueue.default().finishTransaction, onPurchased: ((SKPaymentTransaction) -> Void)? = SKPaymentQueue.default().finishTransaction, onPurchasing: ((SKPaymentTransaction) -> Void)? = nil, onDeferred: ((SKPaymentTransaction) -> Void)? = nil, onRestored: ((SKPaymentTransaction) -> Void)? = nil, onReceiveProducts: (([SKProduct]) -> Void)?) { + super.init(onFailed: onFailed, onPurchased: onPurchased, onPurchasing: onPurchasing, onDeferred: onDeferred, onRestored: onRestored, onReceiveProducts: onReceiveProducts) + } + open func fetchProducts(withIdentifiers identifiers: [String]) { - print(identifiers.count) let request = productIdsToRequest(identifiers) request.delegate = self request.start() onRefresh = identifiers *> fetchProducts } +} + +public class LUXNetCallSubscriptionDelegate: LUXIdSubscriptionDelegate { + var call: T? + public init(call: T, onFailed: ((SKPaymentTransaction) -> Void)? = SKPaymentQueue.default().finishTransaction, + onPurchased: ((SKPaymentTransaction) -> Void)? = SKPaymentQueue.default().finishTransaction, + onPurchasing: ((SKPaymentTransaction) -> Void)? = nil, + onDeferred: ((SKPaymentTransaction) -> Void)? = nil, + onRestored: ((SKPaymentTransaction) -> Void)? = nil, + onReceiveProducts: (([SKProduct]) -> Void)?) { + super.init(onFailed: onFailed, onPurchased: onPurchased, onPurchasing: onPurchasing, onDeferred: onDeferred, onRestored: onRestored, onReceiveProducts: onReceiveProducts) + self.call = call + } - open func fetchProducts(from call: CombineNetCall, unwrapper: @escaping (T) -> [String]) { - self.productsCall = call - let productPub = unwrappedModelPublisher(from: call.publisher.$data.eraseToAnyPublisher(), unwrapper) - productPub.sink(receiveValue: fetchProducts).store(in: &cancelBag) - call.fire() + public override func refresh() { + call?.fire() } - open func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { - print("received response") - DispatchQueue.main.async { - self.products = response.products - } + open func fetchProducts(unwrapper: @escaping (U) -> [String]) { + call?.responder?.dataHandler = ((U.self *-> JsonProvider.decode) -*> ifExecute) >?> (unwrapper >>> fetchProducts) + call?.fire() } +} + +public class LUXCombineSubscriptionDelegate: LUXNetCallSubscriptionDelegate { + var cancelBag: Set = [] - open func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { - transactions.forEach(handleUpdatedTransactions(onFailed: onFailed, onPurchased: onPurchased, onDeferred: onDeferred, onPurchasing: onPurchasing, onRestored: onRestored)) + @Published public var products: [SKProduct]? + + public init(call: CombineNetCall, onFailed: ((SKPaymentTransaction) -> Void)? = SKPaymentQueue.default().finishTransaction, onPurchased: ((SKPaymentTransaction) -> Void)? = SKPaymentQueue.default().finishTransaction, onPurchasing: ((SKPaymentTransaction) -> Void)? = nil, onDeferred: ((SKPaymentTransaction) -> Void)? = nil, onRestored: ((SKPaymentTransaction) -> Void)? = nil, onReceiveProducts: @escaping (LUXCombineSubscriptionDelegate, [SKProduct]) -> Void = setter(\.products)) { + super.init(call: call, onFailed: onFailed, onPurchased: onPurchased, onPurchasing: onPurchasing, onDeferred: onDeferred, onRestored: onRestored, onReceiveProducts: nil) + self.onReceiveProducts = self *-> onReceiveProducts + } + + public override func fetchProducts(unwrapper: @escaping (U) -> [String]){ + unwrappedModelPublisher(from: call?.publisher.$data.eraseToAnyPublisher(), unwrapper)?.sink(receiveValue: fetchProducts).store(in: &cancelBag) } } @@ -74,10 +106,8 @@ public func handleUpdatedTransactions(onFailed: ((SKPaymentTransaction) -> Void) return { transaction in switch transaction.transactionState { case .failed: - SKPaymentQueue.default().finishTransaction(transaction) onFailed?(transaction) case .purchased: - SKPaymentQueue.default().finishTransaction(transaction) onPurchased?(transaction) case .purchasing: onPurchasing?(transaction) From fae7bede227ee20c0f8661dccc9e72c3ae387d7f Mon Sep 17 00:00:00 2001 From: Calvin Collins Date: Mon, 2 Aug 2021 15:32:39 -0700 Subject: [PATCH 5/6] changed generic to standard cell --- .../StoreKit/LUXSubscriptionDelegate.swift | 16 ++++++++++++- .../LUXSubscriptionTableViewCell.swift | 7 ++++++ .../StoreKit/LUXSubscriptionTableViewCell.xib | 23 ++++++++++++++----- .../LUXSubscriptionViewController.swift | 18 ++++++++------- 4 files changed, 49 insertions(+), 15 deletions(-) diff --git a/LUX/Classes/StoreKit/LUXSubscriptionDelegate.swift b/LUX/Classes/StoreKit/LUXSubscriptionDelegate.swift index c112496..2ddc541 100644 --- a/LUX/Classes/StoreKit/LUXSubscriptionDelegate.swift +++ b/LUX/Classes/StoreKit/LUXSubscriptionDelegate.swift @@ -13,6 +13,10 @@ import Prelude import LithoOperators import FunNet +public protocol CombineProductsProvider { + var products: [SKProduct] { get set } +} + public class LUXSubscriptionDelegate: NSObject, SKProductsRequestDelegate, SKPaymentTransactionObserver { public var onReceiveProducts: (([SKProduct]) -> Void)? @@ -63,6 +67,16 @@ public class LUXIdSubscriptionDelegate: LUXSubscriptionDelegate, Refreshable { } } +public class LUXLocalIdSubscriptionDelegate: LUXIdSubscriptionDelegate, CombineProductsProvider { + @Published public var products: [SKProduct] = [] + + public init(onFailed: ((SKPaymentTransaction) -> Void)? = SKPaymentQueue.default().finishTransaction, onPurchased: ((SKPaymentTransaction) -> Void)? = SKPaymentQueue.default().finishTransaction, onPurchasing: ((SKPaymentTransaction) -> Void)? = nil, onDeferred: ((SKPaymentTransaction) -> Void)? = nil, onRestored: ((SKPaymentTransaction) -> Void)? = nil, onReceiveProducts: @escaping (LUXLocalIdSubscriptionDelegate, [SKProduct]) -> Void = setter(\.products)) { + super.init(onFailed: onFailed, onPurchased: onPurchased, onPurchasing: onPurchasing, onDeferred: onDeferred, onRestored: onRestored, onReceiveProducts: nil) + self.onReceiveProducts = self *-> onReceiveProducts + } + +} + public class LUXNetCallSubscriptionDelegate: LUXIdSubscriptionDelegate { var call: T? public init(call: T, onFailed: ((SKPaymentTransaction) -> Void)? = SKPaymentQueue.default().finishTransaction, @@ -88,7 +102,7 @@ public class LUXNetCallSubscriptionDelegate: LUXIdSub public class LUXCombineSubscriptionDelegate: LUXNetCallSubscriptionDelegate { var cancelBag: Set = [] - @Published public var products: [SKProduct]? + @Published public var products: [SKProduct] = [] public init(call: CombineNetCall, onFailed: ((SKPaymentTransaction) -> Void)? = SKPaymentQueue.default().finishTransaction, onPurchased: ((SKPaymentTransaction) -> Void)? = SKPaymentQueue.default().finishTransaction, onPurchasing: ((SKPaymentTransaction) -> Void)? = nil, onDeferred: ((SKPaymentTransaction) -> Void)? = nil, onRestored: ((SKPaymentTransaction) -> Void)? = nil, onReceiveProducts: @escaping (LUXCombineSubscriptionDelegate, [SKProduct]) -> Void = setter(\.products)) { super.init(call: call, onFailed: onFailed, onPurchased: onPurchased, onPurchasing: onPurchasing, onDeferred: onDeferred, onRestored: onRestored, onReceiveProducts: nil) diff --git a/LUX/Classes/StoreKit/LUXSubscriptionTableViewCell.swift b/LUX/Classes/StoreKit/LUXSubscriptionTableViewCell.swift index c1db7ec..1000f43 100644 --- a/LUX/Classes/StoreKit/LUXSubscriptionTableViewCell.swift +++ b/LUX/Classes/StoreKit/LUXSubscriptionTableViewCell.swift @@ -10,4 +10,11 @@ import UIKit public class LUXSubscriptionTableViewCell: UITableViewCell { @IBOutlet weak var subscriptionNameLabel: UILabel! @IBOutlet weak var subscriptionDescriptionLabel: UILabel! + @IBOutlet weak var priceButton: UIButton! + + var onPriceTap: (() -> Void)? + + @IBAction func priceTapped(_ sender: UIButton!) { + onPriceTap?() + } } diff --git a/LUX/Classes/StoreKit/LUXSubscriptionTableViewCell.xib b/LUX/Classes/StoreKit/LUXSubscriptionTableViewCell.xib index dafa58d..bfd7979 100644 --- a/LUX/Classes/StoreKit/LUXSubscriptionTableViewCell.xib +++ b/LUX/Classes/StoreKit/LUXSubscriptionTableViewCell.xib @@ -10,15 +10,15 @@ - - + + - + + + + + + - + diff --git a/LUX/Classes/StoreKit/LUXSubscriptionViewController.swift b/LUX/Classes/StoreKit/LUXSubscriptionViewController.swift index 41b7fb2..52538be 100644 --- a/LUX/Classes/StoreKit/LUXSubscriptionViewController.swift +++ b/LUX/Classes/StoreKit/LUXSubscriptionViewController.swift @@ -11,9 +11,10 @@ import StoreKit import Combine import LithoOperators import PlaygroundVCHelpers +import Prelude -public func subscriptionViewController(styleVC: @escaping (LUXSubscriptionViewController) -> Void, configureCell: @escaping (SKProduct, C) -> Void = configureSubscriptionCell, onTap: @escaping (SKProduct) -> Void = productToPayment >?> addPayment, termsPressed: (() -> Void)? = nil, delegate: LUXSubscriptionDelegate = LUXSubscriptionDelegate()) -> LUXSubscriptionViewController{ - let vc = LUXSubscriptionViewController.makeFromXIB() +public func subscriptionViewController(styleVC: @escaping (LUXSubscriptionViewController) -> Void, configureCell: @escaping (SKProduct, C) -> Void = ~second(optionalCast) >>> ~configureSubscriptionCell, onTap: @escaping (SKProduct) -> Void = productToPayment >?> addPayment, termsPressed: (() -> Void)? = nil, delegate: LUXCombineSubscriptionDelegate) -> LUXSubscriptionViewController { + let vc = LUXSubscriptionViewController.makeFromXIB() let vm = subscriptionViewModel(configureCell: configureCell, onTap: onTap, delegate: delegate) vc.onViewDidLoad = { vm.tableView = $0.tableView @@ -24,15 +25,15 @@ public func subscriptionViewController(styleVC: return vc } -public func subscriptionViewModel(configureCell: @escaping (SKProduct, C) -> Void = configureSubscriptionCell, onTap: @escaping (SKProduct) -> Void = productToPayment >?> addPayment, delegate: LUXSubscriptionDelegate = LUXSubscriptionDelegate()) -> LUXItemsTableViewModel { +public func subscriptionViewModel(configureCell: @escaping (SKProduct, C) -> Void = ~second(optionalCast) >>> ~configureSubscriptionCell, onTap: @escaping (SKProduct) -> Void = productToPayment >?> addPayment, delegate: LUXCombineSubscriptionDelegate) -> LUXItemsTableViewModel { let modelToItem = tappableModelItem(configureCell, onTap: onTap) -*> map return LUXItemsTableViewModel(delegate, itemsPublisher: delegate.$products.map(modelToItem).eraseToAnyPublisher()) } -public class LUXSubscriptionViewController: FUITableViewViewController { +public class LUXSubscriptionViewController: FUITableViewViewController { @IBOutlet weak var termsButton: UIButton! - open var subscriptionsDelegate = LUXSubscriptionDelegate() + open var subscriptionsDelegate: T? public var onTermsPressed: (() -> Void)? @@ -46,9 +47,10 @@ public func productToPayment(product: SKProduct) -> (SKPayment?) { } public let addPayment: (SKPayment) -> Void = SKPaymentQueue.default().add -public func configureSubscriptionCell(product: SKProduct, cell: LUXSubscriptionTableViewCell) { - cell.subscriptionNameLabel.text = "\(product.localizedTitle)" - cell.subscriptionDescriptionLabel.text = "\(product.localizedDescription)" +public func configureSubscriptionCell(product: SKProduct, cell: LUXSubscriptionTableViewCell?) { + cell?.subscriptionNameLabel.text = "\(product.localizedTitle)" + cell?.subscriptionDescriptionLabel.text = "\(product.localizedDescription)" + cell?.priceButton.setTitle(productToPriceString(product), for: .normal) } public func productToPriceString(_ product: SKProduct) -> String { From d18d93090c09f46588599a79383a2ac930df5413 Mon Sep 17 00:00:00 2001 From: Calvin Collins Date: Mon, 2 Aug 2021 15:40:47 -0700 Subject: [PATCH 6/6] clean up price string fns --- .../LUXSubscriptionViewController.swift | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/LUX/Classes/StoreKit/LUXSubscriptionViewController.swift b/LUX/Classes/StoreKit/LUXSubscriptionViewController.swift index 52538be..2b69d5a 100644 --- a/LUX/Classes/StoreKit/LUXSubscriptionViewController.swift +++ b/LUX/Classes/StoreKit/LUXSubscriptionViewController.swift @@ -53,24 +53,27 @@ public func configureSubscriptionCell(product: SKProduct, cell: LUXSubscriptionT cell?.priceButton.setTitle(productToPriceString(product), for: .normal) } -public func productToPriceString(_ product: SKProduct) -> String { - let currency = product.priceLocale.currencySymbol ?? "" - let price = product.price +public let productToPriceString: (SKProduct) -> String = fzip(currency, price, productTimeFrame) >>> combinePriceString + +public func productTimeFrame(_ product: SKProduct) -> String { let period = product.subscriptionPeriod?.numberOfUnits - var unit: String? switch product.subscriptionPeriod?.unit { case .month: if (period != nil && period != 1) { - unit = "\(period!) Months" + return "\(period!) Months" } else { - unit = "Month" + return "Month" } case .year: - unit = "Year" + return "Year" case .none: - unit = "" + return "" default: - unit = "" + return "" } - return "\(currency)\(price)/\(unit ?? "")" } + + +let currency: (SKProduct) -> String = ^\SKProduct.priceLocale.currencySymbol >>> coalesceNil(with: "") +let price = ^\SKProduct.price +let combinePriceString: ((String, NSDecimalNumber, String)) -> String = { "\($0.0)\($0.1)/\($0.2)" }