From 5c1dd22b96b1cd7b72f7f8bc6053d09215f9e210 Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Sun, 7 Feb 2021 10:43:54 +0100 Subject: [PATCH 1/7] Simplify feature eligibility --- .../TrustedNetworksViewController.swift | 4 +- .../Sources/Model/ConnectionService.swift | 2 +- .../Core/Sources/Model/ProductManager.swift | 41 ++++++------------- 3 files changed, 15 insertions(+), 32 deletions(-) diff --git a/Passepartout/App/macOS/Scenes/Service/Customization/TrustedNetworksViewController.swift b/Passepartout/App/macOS/Scenes/Service/Customization/TrustedNetworksViewController.swift index cf5faf08..c7be314c 100644 --- a/Passepartout/App/macOS/Scenes/Service/Customization/TrustedNetworksViewController.swift +++ b/Passepartout/App/macOS/Scenes/Service/Customization/TrustedNetworksViewController.swift @@ -108,7 +108,7 @@ class TrustedNetworksViewController: NSViewController, ProfileCustomization { @IBAction private func toggleTrustEthernet(_ sender: Any?) { do { - try ProductManager.shared.verifyEligibleForTrustedNetworks() + try ProductManager.shared.verifyEligible(forFeature: .trustedNetworks) } catch { checkTrustEthernet.state = .off presentPurchaseScreen(forProduct: .trustedNetworks) @@ -132,7 +132,7 @@ class TrustedNetworksViewController: NSViewController, ProfileCustomization { override func shouldPerformSegue(withIdentifier identifier: NSStoryboardSegue.Identifier, sender: Any?) -> Bool { if identifier == StoryboardSegue.Service.trustedNetworkAddSegueIdentifier.rawValue { do { - try ProductManager.shared.verifyEligibleForTrustedNetworks() + try ProductManager.shared.verifyEligible(forFeature: .trustedNetworks) } catch { presentPurchaseScreen(forProduct: .trustedNetworks) return false diff --git a/Passepartout/Core/Sources/Model/ConnectionService.swift b/Passepartout/Core/Sources/Model/ConnectionService.swift index 4813f70a..38cb2081 100644 --- a/Passepartout/Core/Sources/Model/ConnectionService.swift +++ b/Passepartout/Core/Sources/Model/ConnectionService.swift @@ -554,7 +554,7 @@ public class ConnectionService: Codable { var rules: [NEOnDemandRule] = [] do { - try ProductManager.shared.verifyEligibleForTrustedNetworks() + try ProductManager.shared.verifyEligible(forFeature: .trustedNetworks) #if os(iOS) if profile.trustedNetworks.includesMobile { let rule = policyRule(for: profile) diff --git a/Passepartout/Core/Sources/Model/ProductManager.swift b/Passepartout/Core/Sources/Model/ProductManager.swift index 62b9dc14..de9e692d 100644 --- a/Passepartout/Core/Sources/Model/ProductManager.swift +++ b/Passepartout/Core/Sources/Model/ProductManager.swift @@ -159,16 +159,21 @@ public class ProductManager: NSObject { // MARK: In-app eligibility - public func isFullVersion() -> Bool { + private func isCurrentPlatformVersion() -> Bool { #if os(iOS) - if (isBeta && cfg.isBetaFullVersion) || purchasedFeatures.contains(.fullVersion_iOS) { - return true - } + return purchasedFeatures.contains(.fullVersion_iOS) #else - if (isBeta && cfg.isBetaFullVersion) || purchasedFeatures.contains(.fullVersion_macOS) { + return purchasedFeatures.contains(.fullVersion_macOS) + #endif + } + + private func isFullVersion() -> Bool { + if isBeta && cfg.isBetaFullVersion { + return true + } + if isCurrentPlatformVersion() { return true } - #endif return purchasedFeatures.contains(.fullVersion) } @@ -176,14 +181,6 @@ public class ProductManager: NSObject { return isFullVersion() || purchasedFeatures.contains(feature) } - private func isEligible(forProvider metadata: Infrastructure.Metadata) -> Bool { - return isFullVersion() || purchasedFeatures.contains(metadata.product) - } - - private func isEligibleForTrustedNetworks() -> Bool { - return isFullVersion() || purchasedFeatures.contains(.trustedNetworks) - } - public func isEligibleForFeedback() -> Bool { return isBeta || !purchasedFeatures.isEmpty } @@ -211,21 +208,7 @@ public class ProductManager: NSObject { throw ProductError.beta } } - guard isFullVersion() || purchasedFeatures.contains(metadata.product) else { - throw ProductError.uneligible - } - } - - public func verifyEligibleForTrustedNetworks() throws { - if isBeta { - if cfg.isBetaFullVersion { - return - } - guard !cfg.locksBetaFeatures else { - throw ProductError.beta - } - } - guard isEligibleForTrustedNetworks() else { + guard isEligible(forFeature: metadata.product) else { throw ProductError.uneligible } } From 9d0bddfc3ca97665fa1c21d2d4f180207da4995e Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Sun, 7 Feb 2021 15:18:55 +0100 Subject: [PATCH 2/7] Ack single features on iOS only --- Passepartout/Core/Sources/Model/ProductManager.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Passepartout/Core/Sources/Model/ProductManager.swift b/Passepartout/Core/Sources/Model/ProductManager.swift index de9e692d..bd1cb517 100644 --- a/Passepartout/Core/Sources/Model/ProductManager.swift +++ b/Passepartout/Core/Sources/Model/ProductManager.swift @@ -178,7 +178,11 @@ public class ProductManager: NSObject { } private func isEligible(forFeature feature: Product) -> Bool { + #if os(iOS) return isFullVersion() || purchasedFeatures.contains(feature) + #else + return isFullVersion() + #endif } public func isEligibleForFeedback() -> Bool { From 47da4ba5afca44d8d6cf8c106b532bdcef32a83f Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Sun, 7 Feb 2021 11:16:57 +0100 Subject: [PATCH 3/7] Make feature purchase optional --- .../Organizer/WizardProviderViewController.swift | 2 +- .../iOS/Scenes/Purchase/PurchaseViewController.swift | 12 ++++-------- .../Scenes/Purchase/PurchaseViewController.swift | 12 ++++-------- 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/Passepartout/App/iOS/Scenes/Organizer/WizardProviderViewController.swift b/Passepartout/App/iOS/Scenes/Organizer/WizardProviderViewController.swift index 419eba88..a06a6fdb 100644 --- a/Passepartout/App/iOS/Scenes/Organizer/WizardProviderViewController.swift +++ b/Passepartout/App/iOS/Scenes/Organizer/WizardProviderViewController.swift @@ -230,7 +230,7 @@ extension WizardProviderViewController: AccountViewControllerDelegate { // MARK: - extension WizardProviderViewController: PurchaseViewControllerDelegate { - func purchaseController(_ purchaseController: PurchaseViewController, didPurchase product: Product) { + func purchaseController(_ purchaseController: PurchaseViewController, didPurchase product: Product?) { guard let metadata = selectedMetadata else { return } diff --git a/Passepartout/App/iOS/Scenes/Purchase/PurchaseViewController.swift b/Passepartout/App/iOS/Scenes/Purchase/PurchaseViewController.swift index 19b1809e..a5c48e38 100644 --- a/Passepartout/App/iOS/Scenes/Purchase/PurchaseViewController.swift +++ b/Passepartout/App/iOS/Scenes/Purchase/PurchaseViewController.swift @@ -32,13 +32,13 @@ import Convenience private let log = SwiftyBeaver.self protocol PurchaseViewControllerDelegate: class { - func purchaseController(_ purchaseController: PurchaseViewController, didPurchase product: Product) + func purchaseController(_ purchaseController: PurchaseViewController, didPurchase product: Product?) } class PurchaseViewController: UITableViewController, StrongTableHost { private var isLoading = true - var feature: Product! + var feature: Product? weak var delegate: PurchaseViewControllerDelegate? @@ -71,7 +71,7 @@ class PurchaseViewController: UITableViewController, StrongTableHost { self.skFullVersion = skFullVersion rows.append(.fullVersion) } - if let skFeature = pm.product(withIdentifier: feature) { + if let feature = feature, let skFeature = pm.product(withIdentifier: feature) { self.skFeature = skFeature rows.append(.feature) } @@ -96,10 +96,6 @@ class PurchaseViewController: UITableViewController, StrongTableHost { override func viewDidLoad() { super.viewDidLoad() - guard let _ = feature else { - fatalError("No feature set for purchase") - } - title = L10n.Core.Purchase.title navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .stop, target: self, action: #selector(close)) @@ -168,7 +164,7 @@ class PurchaseViewController: UITableViewController, StrongTableHost { guard let weakSelf = self else { return } - let product = weakSelf.feature.matchesStoreKitProduct(skProduct) ? weakSelf.feature! : .fullVersion + let product = Product(rawValue: skProduct.productIdentifier) weakSelf.delegate?.purchaseController(weakSelf, didPurchase: product) } } diff --git a/Passepartout/App/macOS/Scenes/Purchase/PurchaseViewController.swift b/Passepartout/App/macOS/Scenes/Purchase/PurchaseViewController.swift index 33580def..65bfb8ff 100644 --- a/Passepartout/App/macOS/Scenes/Purchase/PurchaseViewController.swift +++ b/Passepartout/App/macOS/Scenes/Purchase/PurchaseViewController.swift @@ -32,7 +32,7 @@ import Convenience private let log = SwiftyBeaver.self protocol PurchaseViewControllerDelegate: class { - func purchaseController(_ purchaseController: PurchaseViewController, didPurchase product: Product) + func purchaseController(_ purchaseController: PurchaseViewController, didPurchase product: Product?) } class PurchaseViewController: NSViewController { @@ -52,7 +52,7 @@ class PurchaseViewController: NSViewController { @IBOutlet private weak var buttonRestore: NSButton! - var feature: Product! + var feature: Product? weak var delegate: PurchaseViewControllerDelegate? @@ -79,7 +79,7 @@ class PurchaseViewController: NSViewController { self.skFullVersion = skFullVersion rows.append(.fullVersion) } - if let skFeature = pm.product(withIdentifier: feature) { + if let feature = feature, let skFeature = pm.product(withIdentifier: feature) { self.skFeature = skFeature rows.append(.feature) } @@ -108,10 +108,6 @@ class PurchaseViewController: NSViewController { buttonPurchase.title = L10n.Core.Purchase.title buttonRestore.title = L10n.Core.Purchase.Cells.Restore.title - guard let _ = feature else { - fatalError("No feature set for purchase") - } - tableView.usesAutomaticRowHeights = true tableView.reloadData() } @@ -197,7 +193,7 @@ class PurchaseViewController: NSViewController { guard let weakSelf = self else { return } - let product = weakSelf.feature.matchesStoreKitProduct(skProduct) ? weakSelf.feature! : .fullVersion + let product = Product(rawValue: skProduct.productIdentifier) weakSelf.delegate?.purchaseController(weakSelf, didPurchase: product) self?.dismiss(nil) From 0c7b88f2c5fef17328412e83899d031ff653dcb8 Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Sun, 7 Feb 2021 11:50:48 +0100 Subject: [PATCH 4/7] Drop single feature purchase --- Passepartout/App/iOS/Global/Macros.swift | 2 +- Passepartout/App/macOS/Global/Macros.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Passepartout/App/iOS/Global/Macros.swift b/Passepartout/App/iOS/Global/Macros.swift index 9b5fcc4a..5c89c723 100644 --- a/Passepartout/App/iOS/Global/Macros.swift +++ b/Passepartout/App/iOS/Global/Macros.swift @@ -67,7 +67,7 @@ extension UIViewController { func presentPurchaseScreen(forProduct product: Product, delegate: PurchaseViewControllerDelegate? = nil) { let nav = StoryboardScene.Purchase.initialScene.instantiate() let vc = nav.topViewController as? PurchaseViewController - vc?.feature = product +// vc?.feature = product vc?.delegate = delegate // enforce pre iOS 13 behavior diff --git a/Passepartout/App/macOS/Global/Macros.swift b/Passepartout/App/macOS/Global/Macros.swift index 4f32f6e5..cbe6556e 100644 --- a/Passepartout/App/macOS/Global/Macros.swift +++ b/Passepartout/App/macOS/Global/Macros.swift @@ -93,7 +93,7 @@ extension NSAlert { extension NSViewController { func presentPurchaseScreen(forProduct product: Product, delegate: PurchaseViewControllerDelegate? = nil) { let vc = StoryboardScene.Purchase.initialScene.instantiate() - vc.feature = product +// vc.feature = product vc.delegate = delegate presentAsModalWindow(vc) } From c9577eb3fdc4eca80af01e000b241d6a1074f1a2 Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Sun, 7 Feb 2021 12:48:27 +0100 Subject: [PATCH 5/7] Add dummy "All providers" purchase --- Passepartout/Core/Sources/Model/Product.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Passepartout/Core/Sources/Model/Product.swift b/Passepartout/Core/Sources/Model/Product.swift index a29aec8d..8ee7cd93 100644 --- a/Passepartout/Core/Sources/Model/Product.swift +++ b/Passepartout/Core/Sources/Model/Product.swift @@ -65,6 +65,8 @@ public struct Product: RawRepresentable, Equatable, Hashable { } // MARK: Features + + public static let allProviders = Product(featureId: "all_providers") public static let trustedNetworks = Product(featureId: "trusted_networks") @@ -77,6 +79,7 @@ public struct Product: RawRepresentable, Equatable, Hashable { public static let fullVersion = Product(featureId: "full_multi_version") public static let allFeatures: [Product] = [ + .allProviders, .trustedNetworks, .siriShortcuts, .fullVersion_iOS, From 96189b410f7ff9342f681804917f3d7a96e868ec Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Sun, 7 Feb 2021 12:50:39 +0100 Subject: [PATCH 6/7] Review product bullets - Show features in platform - Show iOS/macOS in multiplatform Drop dashes in iOS. --- .../Purchase/PurchaseViewController.swift | 22 +++++++++---------- .../Purchase/PurchaseViewController.swift | 22 +++++++++---------- .../Core/Sources/Model/ProductManager.swift | 15 +++++++++++++ 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/Passepartout/App/iOS/Scenes/Purchase/PurchaseViewController.swift b/Passepartout/App/iOS/Scenes/Purchase/PurchaseViewController.swift index a5c48e38..d9739e74 100644 --- a/Passepartout/App/iOS/Scenes/Purchase/PurchaseViewController.swift +++ b/Passepartout/App/iOS/Scenes/Purchase/PurchaseViewController.swift @@ -66,10 +66,20 @@ class PurchaseViewController: UITableViewController, StrongTableHost { if let skPlatformVersion = pm.product(withIdentifier: .fullVersion_iOS) { self.skPlatformVersion = skPlatformVersion rows.append(.platformVersion) + + let bullets: [String] = ProductManager.shared.featureProducts(excluding: [.fullVersion, .fullVersion_iOS, .fullVersion_macOS]).map { + return $0.localizedTitle + }.sortedCaseInsensitive() + platformVersionExtra = bullets.joined(separator: "\n") } if let skFullVersion = pm.product(withIdentifier: .fullVersion) { self.skFullVersion = skFullVersion rows.append(.fullVersion) + + let bullets: [String] = ProductManager.shared.featureProducts(including: [.fullVersion_iOS, .fullVersion_macOS]).map { + return $0.localizedTitle + }.sortedCaseInsensitive() + fullVersionExtra = bullets.joined(separator: "\n") } if let feature = feature, let skFeature = pm.product(withIdentifier: feature) { self.skFeature = skFeature @@ -77,18 +87,6 @@ class PurchaseViewController: UITableViewController, StrongTableHost { } rows.append(.restore) model.set(rows, forSection: .products) - - let platformBulletsList: [String] = ProductManager.shared.featureProducts(excluding: [.fullVersion, .fullVersion_iOS, .fullVersion_macOS]).map { - return "- \($0.localizedTitle)" - }.sortedCaseInsensitive() - let platformBullets = platformBulletsList.joined(separator: "\n") - platformVersionExtra = "- \(L10n.Core.Purchase.Cells.FullVersion.extraDescription(platformBullets))" - - let fullBulletsList: [String] = ProductManager.shared.featureProducts(excluding: [.fullVersion, .fullVersion_iOS]).map { - return "- \($0.localizedTitle)" - }.sortedCaseInsensitive() - let fullBullets = fullBulletsList.joined(separator: "\n") - fullVersionExtra = "- \(L10n.Core.Purchase.Cells.FullVersion.extraDescription(fullBullets))" } // MARK: UIViewController diff --git a/Passepartout/App/macOS/Scenes/Purchase/PurchaseViewController.swift b/Passepartout/App/macOS/Scenes/Purchase/PurchaseViewController.swift index 65bfb8ff..371e7402 100644 --- a/Passepartout/App/macOS/Scenes/Purchase/PurchaseViewController.swift +++ b/Passepartout/App/macOS/Scenes/Purchase/PurchaseViewController.swift @@ -74,27 +74,25 @@ class PurchaseViewController: NSViewController { if let skPlatformVersion = pm.product(withIdentifier: .fullVersion_macOS) { self.skPlatformVersion = skPlatformVersion rows.append(.platformVersion) + + let bullets: [String] = ProductManager.shared.featureProducts(excluding: [.fullVersion, .fullVersion_iOS, .fullVersion_macOS, .siriShortcuts]).map { + return $0.localizedTitle + }.sortedCaseInsensitive() + platformVersionExtra = bullets.joined(separator: "\n") } if let skFullVersion = pm.product(withIdentifier: .fullVersion) { self.skFullVersion = skFullVersion rows.append(.fullVersion) + + let bullets: [String] = ProductManager.shared.featureProducts(including: [.fullVersion_iOS, .fullVersion_macOS]).map { + return $0.localizedTitle + }.sortedCaseInsensitive() + fullVersionExtra = bullets.joined(separator: "\n") } if let feature = feature, let skFeature = pm.product(withIdentifier: feature) { self.skFeature = skFeature rows.append(.feature) } - - let platformBulletsList: [String] = ProductManager.shared.featureProducts(excluding: [.siriShortcuts, .fullVersion, .fullVersion_iOS, .fullVersion_macOS]).map { - return $0.localizedTitle - }.sortedCaseInsensitive() - let platformBullets = platformBulletsList.joined(separator: "\n") - platformVersionExtra = L10n.Core.Purchase.Cells.FullVersion.extraDescription(platformBullets) - - let fullBulletsList: [String] = ProductManager.shared.featureProducts(excluding: [.fullVersion, .fullVersion_macOS]).map { - return $0.localizedTitle - }.sortedCaseInsensitive() - let fullBullets = fullBulletsList.joined(separator: "\n") - fullVersionExtra = L10n.Core.Purchase.Cells.FullVersion.extraDescription(fullBullets) } // MARK: NSViewController diff --git a/Passepartout/Core/Sources/Model/ProductManager.swift b/Passepartout/Core/Sources/Model/ProductManager.swift index bd1cb517..1693ac49 100644 --- a/Passepartout/Core/Sources/Model/ProductManager.swift +++ b/Passepartout/Core/Sources/Model/ProductManager.swift @@ -126,6 +126,21 @@ public class ProductManager: NSObject { return inApp.product(withIdentifier: identifier) } + public func featureProducts(including: [Product]) -> [SKProduct] { + return inApp.products.filter { + guard let p = Product(rawValue: $0.productIdentifier) else { + return false + } + guard including.contains(p) else { + return false + } + guard p.isFeature else { + return false + } + return true + } + } + public func featureProducts(excluding: [Product]) -> [SKProduct] { return inApp.products.filter { guard let p = Product(rawValue: $0.productIdentifier) else { From a41aa6d35ea83b2a914ee5c9fa14f3494b7bb272 Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Sun, 7 Feb 2021 15:28:30 +0100 Subject: [PATCH 7/7] Prevent multi-platform purchase when redundant --- .../App/iOS/Scenes/Purchase/PurchaseViewController.swift | 2 +- .../App/macOS/Scenes/Purchase/PurchaseViewController.swift | 2 +- Passepartout/Core/Sources/Model/ProductManager.swift | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Passepartout/App/iOS/Scenes/Purchase/PurchaseViewController.swift b/Passepartout/App/iOS/Scenes/Purchase/PurchaseViewController.swift index d9739e74..661141a2 100644 --- a/Passepartout/App/iOS/Scenes/Purchase/PurchaseViewController.swift +++ b/Passepartout/App/iOS/Scenes/Purchase/PurchaseViewController.swift @@ -72,7 +72,7 @@ class PurchaseViewController: UITableViewController, StrongTableHost { }.sortedCaseInsensitive() platformVersionExtra = bullets.joined(separator: "\n") } - if let skFullVersion = pm.product(withIdentifier: .fullVersion) { + if !pm.hasPurchased(.fullVersion_macOS), let skFullVersion = pm.product(withIdentifier: .fullVersion) { self.skFullVersion = skFullVersion rows.append(.fullVersion) diff --git a/Passepartout/App/macOS/Scenes/Purchase/PurchaseViewController.swift b/Passepartout/App/macOS/Scenes/Purchase/PurchaseViewController.swift index 371e7402..32287638 100644 --- a/Passepartout/App/macOS/Scenes/Purchase/PurchaseViewController.swift +++ b/Passepartout/App/macOS/Scenes/Purchase/PurchaseViewController.swift @@ -80,7 +80,7 @@ class PurchaseViewController: NSViewController { }.sortedCaseInsensitive() platformVersionExtra = bullets.joined(separator: "\n") } - if let skFullVersion = pm.product(withIdentifier: .fullVersion) { + if !pm.hasPurchased(.fullVersion_iOS), let skFullVersion = pm.product(withIdentifier: .fullVersion) { self.skFullVersion = skFullVersion rows.append(.fullVersion) diff --git a/Passepartout/Core/Sources/Model/ProductManager.swift b/Passepartout/Core/Sources/Model/ProductManager.swift index 1693ac49..62e54dd7 100644 --- a/Passepartout/Core/Sources/Model/ProductManager.swift +++ b/Passepartout/Core/Sources/Model/ProductManager.swift @@ -232,6 +232,10 @@ public class ProductManager: NSObject { } } + public func hasPurchased(_ product: Product) -> Bool { + return purchasedFeatures.contains(product) + } + public func isCancelledPurchase(_ product: Product) -> Bool { return cancelledPurchases.contains(product) }