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/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/LUX.podspec b/LUX.podspec index 2c90988..0328b79 100644 --- a/LUX.podspec +++ b/LUX.podspec @@ -133,4 +133,13 @@ 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' + sp.dependency 'PlaygroundVCHelpers' + end + end diff --git a/LUX/Classes/StoreKit/LUXSubscriptionDelegate.swift b/LUX/Classes/StoreKit/LUXSubscriptionDelegate.swift new file mode 100644 index 0000000..2ddc541 --- /dev/null +++ b/LUX/Classes/StoreKit/LUXSubscriptionDelegate.swift @@ -0,0 +1,136 @@ +// +// 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 protocol CombineProductsProvider { + var products: [SKProduct] { get set } +} + +public class LUXSubscriptionDelegate: NSObject, SKProductsRequestDelegate, SKPaymentTransactionObserver { + public var onReceiveProducts: (([SKProduct]) -> Void)? + + 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 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]) { + let request = productIdsToRequest(identifiers) + request.delegate = self + request.start() + onRefresh = identifiers *> fetchProducts + } +} + +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, + 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 + } + + public override func refresh() { + call?.fire() + } + + 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 = [] + + @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) + } +} + +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: + onFailed?(transaction) + case .purchased: + onPurchased?(transaction) + case .purchasing: + onPurchasing?(transaction) + case .deferred: + onDeferred?(transaction) + case .restored: + onRestored?(transaction) + @unknown default: + break + } + } +} diff --git a/LUX/Classes/StoreKit/LUXSubscriptionTableViewCell.swift b/LUX/Classes/StoreKit/LUXSubscriptionTableViewCell.swift new file mode 100644 index 0000000..1000f43 --- /dev/null +++ b/LUX/Classes/StoreKit/LUXSubscriptionTableViewCell.swift @@ -0,0 +1,20 @@ +// +// 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! + @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 new file mode 100644 index 0000000..bfd7979 --- /dev/null +++ b/LUX/Classes/StoreKit/LUXSubscriptionTableViewCell.xib @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LUX/Classes/StoreKit/LUXSubscriptionViewController.swift b/LUX/Classes/StoreKit/LUXSubscriptionViewController.swift new file mode 100644 index 0000000..2b69d5a --- /dev/null +++ b/LUX/Classes/StoreKit/LUXSubscriptionViewController.swift @@ -0,0 +1,79 @@ +// +// SubscriptionViewController.swift +// LUX +// +// Created by Calvin Collins on 7/26/21. +// + +import UIKit +import fuikit +import StoreKit +import Combine +import LithoOperators +import PlaygroundVCHelpers +import Prelude + +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 + vm.tableView?.reloadData() + vm.refresh() + } + vc.onTermsPressed = termsPressed + return vc +} + +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 { + @IBOutlet weak var termsButton: UIButton! + + open var subscriptionsDelegate: T? + + 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)" + cell?.priceButton.setTitle(productToPriceString(product), for: .normal) +} + +public let productToPriceString: (SKProduct) -> String = fzip(currency, price, productTimeFrame) >>> combinePriceString + +public func productTimeFrame(_ product: SKProduct) -> String { + let period = product.subscriptionPeriod?.numberOfUnits + switch product.subscriptionPeriod?.unit { + case .month: + if (period != nil && period != 1) { + return "\(period!) Months" + } else { + return "Month" + } + case .year: + return "Year" + case .none: + return "" + default: + return "" + } +} + + +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)" } diff --git a/LUX/Classes/StoreKit/LUXSubscriptionViewController.xib b/LUX/Classes/StoreKit/LUXSubscriptionViewController.xib new file mode 100644 index 0000000..311fbc7 --- /dev/null +++ b/LUX/Classes/StoreKit/LUXSubscriptionViewController.xib @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +