diff --git a/Passepartout/App/iOS/AppDelegate.swift b/Passepartout/App/iOS/AppDelegate.swift index f47c3380..e36aac87 100644 --- a/Passepartout/App/iOS/AppDelegate.swift +++ b/Passepartout/App/iOS/AppDelegate.swift @@ -105,12 +105,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele fatalError("No window.rootViewController?") } let topmost = root.presentedViewController ?? root - if TransientStore.shared.service.hasReachedMaximumNumberOfHosts { - guard ProductManager.shared.isEligible(forFeature: .unlimitedHosts) else { - topmost.presentPurchaseScreen(forProduct: .unlimitedHosts) - return false - } - } return tryParseURL(url, passphrase: nil, target: topmost) } @@ -168,7 +162,7 @@ extension UISplitViewController { extension AppDelegate { func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { - guard ProductManager.shared.isEligible(forFeature: .siriShortcuts) else { + guard (try? ProductManager.shared.isEligible(forFeature: .siriShortcuts)) ?? false else { return false } guard let interaction = userActivity.interaction else { diff --git a/Passepartout/App/iOS/CHANGELOG.md b/Passepartout/App/iOS/CHANGELOG.md index 3144ab16..158efe50 100644 --- a/Passepartout/App/iOS/CHANGELOG.md +++ b/Passepartout/App/iOS/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Changed + +- Drop hosts restriction in free version ("Unlimited hosts"). + ## 1.14.0 (2021-01-07) ### Added diff --git a/Passepartout/App/iOS/Global/AppConstants+App.swift b/Passepartout/App/iOS/Global/AppConstants+App.swift index 0c02f749..8094f689 100644 --- a/Passepartout/App/iOS/Global/AppConstants+App.swift +++ b/Passepartout/App/iOS/Global/AppConstants+App.swift @@ -32,13 +32,10 @@ extension AppConstants { } struct InApp { -// static var isBetaFullVersion: Bool { -// return ProcessInfo.processInfo.environment["FULL_VERSION"] != nil -// } - static let isBetaFullVersion = true + static var isBetaFullVersion: Bool { + return ProcessInfo.processInfo.environment["FULL_VERSION"] != nil + } static let lastFullVersionBuild = 2016 - - static let limitedNumberOfHosts = 2 } } diff --git a/Passepartout/App/iOS/Global/Macros.swift b/Passepartout/App/iOS/Global/Macros.swift index a84bddf4..9b5fcc4a 100644 --- a/Passepartout/App/iOS/Global/Macros.swift +++ b/Passepartout/App/iOS/Global/Macros.swift @@ -75,6 +75,12 @@ extension UIViewController { present(nav, animated: true, completion: nil) } + + func presentBetaFeatureUnavailable(_ title: String) { + let alert = UIAlertController.asAlert(title, "The requested feature is unavailable in beta.") + alert.addCancelAction("OK") + present(alert, animated: true, completion: nil) + } } func visitURL(_ url: URL) { diff --git a/Passepartout/App/iOS/Global/ProductManager+App.swift b/Passepartout/App/iOS/Global/ProductManager+App.swift index 0571cffb..364f4ecd 100644 --- a/Passepartout/App/iOS/Global/ProductManager+App.swift +++ b/Passepartout/App/iOS/Global/ProductManager+App.swift @@ -46,7 +46,7 @@ extension ProductManager { // review features and potentially revert them if they were used (Siri is handled in AppDelegate) log.debug("Checking 'Trusted networks'") - if !isEligible(forFeature: .trustedNetworks) { + if !((try? isEligible(forFeature: .trustedNetworks)) ?? false) { // reset trusted networks for ALL profiles (must load first) for key in service.allProfileKeys() { @@ -71,24 +71,12 @@ extension ProductManager { } } - log.debug("Checking 'Unlimited hosts'") - if !isEligible(forFeature: .unlimitedHosts) { - let ids = service.hostIds() - if ids.count > AppConstants.InApp.limitedNumberOfHosts { - for id in ids { - service.removeProfile(ProfileKey(.host, id)) - } - log.debug("\tRefunded") - anyRefund = true - } - } - log.debug("Checking providers") for name in service.providerNames() { guard let metadata = InfrastructureFactory.shared.metadata(forName: name) else { continue } - if !isEligible(forProvider: metadata) { + if !((try? isEligible(forProvider: metadata)) ?? false) { service.removeProfile(ProfileKey(name)) log.debug("\tRefunded provider: \(name)") anyRefund = true @@ -108,10 +96,3 @@ extension ProductManager { NotificationCenter.default.post(name: ProductManager.didReviewPurchases, object: nil) } } - -extension ConnectionService { - var hasReachedMaximumNumberOfHosts: Bool { - let numberOfHosts = hostIds().count - return numberOfHosts >= AppConstants.InApp.limitedNumberOfHosts - } -} diff --git a/Passepartout/App/iOS/Scenes/Organizer/OrganizerViewController.swift b/Passepartout/App/iOS/Scenes/Organizer/OrganizerViewController.swift index 8a452f22..cb81072e 100644 --- a/Passepartout/App/iOS/Scenes/Organizer/OrganizerViewController.swift +++ b/Passepartout/App/iOS/Scenes/Organizer/OrganizerViewController.swift @@ -222,12 +222,6 @@ class OrganizerViewController: UITableViewController, StrongTableHost { } private func addNewHost() { - if TransientStore.shared.service.hasReachedMaximumNumberOfHosts { - guard ProductManager.shared.isEligible(forFeature: .unlimitedHosts) else { - presentPurchaseScreen(forProduct: .unlimitedHosts) - return - } - } let picker = UIDocumentPickerViewController(documentTypes: AppConstants.URLs.filetypes, in: .import) picker.allowsMultipleSelection = false picker.delegate = self @@ -243,18 +237,17 @@ class OrganizerViewController: UITableViewController, StrongTableHost { } private func importNewHost() { - if TransientStore.shared.service.hasReachedMaximumNumberOfHosts { - guard ProductManager.shared.isEligible(forFeature: .unlimitedHosts) else { - presentPurchaseScreen(forProduct: .unlimitedHosts) - return - } - } perform(segue: StoryboardSegue.Organizer.showImportedHostsSegueIdentifier) } private func addShortcuts() { - guard ProductManager.shared.isEligible(forFeature: .siriShortcuts) else { - presentPurchaseScreen(forProduct: .siriShortcuts) + do { + guard try ProductManager.shared.isEligible(forFeature: .siriShortcuts) else { + presentPurchaseScreen(forProduct: .siriShortcuts) + return + } + } catch { + presentBetaFeatureUnavailable("Siri") return } perform(segue: StoryboardSegue.Organizer.siriShortcutsSegueIdentifier) diff --git a/Passepartout/App/iOS/Scenes/Organizer/WizardProviderViewController.swift b/Passepartout/App/iOS/Scenes/Organizer/WizardProviderViewController.swift index 907d7206..d48d9b8f 100644 --- a/Passepartout/App/iOS/Scenes/Organizer/WizardProviderViewController.swift +++ b/Passepartout/App/iOS/Scenes/Organizer/WizardProviderViewController.swift @@ -70,11 +70,16 @@ class WizardProviderViewController: UITableViewController, StrongTableHost { private func tryNext(withMetadata metadata: Infrastructure.Metadata, purchaseIfNecessary: Bool) { selectedMetadata = metadata - guard ProductManager.shared.isEligible(forProvider: metadata) else { - guard purchaseIfNecessary else { + do { + guard try ProductManager.shared.isEligible(forProvider: metadata) else { + guard purchaseIfNecessary else { + return + } + presentPurchaseScreen(forProduct: metadata.product, delegate: self) return } - presentPurchaseScreen(forProduct: metadata.product, delegate: self) + } catch { + presentBetaFeatureUnavailable("Providers") return } diff --git a/Passepartout/App/iOS/Scenes/ServiceViewController.swift b/Passepartout/App/iOS/Scenes/ServiceViewController.swift index dafa9b2a..1b76562d 100644 --- a/Passepartout/App/iOS/Scenes/ServiceViewController.swift +++ b/Passepartout/App/iOS/Scenes/ServiceViewController.swift @@ -377,11 +377,19 @@ class ServiceViewController: UIViewController, StrongTableHost { } private func trustMobileNetwork(cell: ToggleTableViewCell) { - guard ProductManager.shared.isEligible(forFeature: .trustedNetworks) else { + do { + guard try ProductManager.shared.isEligible(forFeature: .trustedNetworks) else { + delay { + cell.setOn(false, animated: true) + } + presentPurchaseScreen(forProduct: .trustedNetworks) + return + } + } catch { delay { cell.setOn(false, animated: true) } - presentPurchaseScreen(forProduct: .trustedNetworks) + presentBetaFeatureUnavailable("Trusted networks") return } @@ -394,8 +402,13 @@ class ServiceViewController: UIViewController, StrongTableHost { } private func trustCurrentWiFi() { - guard ProductManager.shared.isEligible(forFeature: .trustedNetworks) else { - presentPurchaseScreen(forProduct: .trustedNetworks) + do { + guard try ProductManager.shared.isEligible(forFeature: .trustedNetworks) else { + presentPurchaseScreen(forProduct: .trustedNetworks) + return + } + } catch { + presentBetaFeatureUnavailable("Trusted networks") return } @@ -447,14 +460,22 @@ class ServiceViewController: UIViewController, StrongTableHost { } private func toggleTrustWiFi(cell: ToggleTableViewCell, at row: Int) { - guard ProductManager.shared.isEligible(forFeature: .trustedNetworks) else { + do { + guard try ProductManager.shared.isEligible(forFeature: .trustedNetworks) else { + delay { + cell.setOn(false, animated: true) + } + presentPurchaseScreen(forProduct: .trustedNetworks) + return + } + } catch { delay { cell.setOn(false, animated: true) } - presentPurchaseScreen(forProduct: .trustedNetworks) + presentBetaFeatureUnavailable("Trusted networks") return } - + if cell.isOn { trustedNetworks.enableWifi(at: row) } else { diff --git a/Passepartout/Core/Sources/Model/Product.swift b/Passepartout/Core/Sources/Model/Product.swift index 0eff5313..ea8a7795 100644 --- a/Passepartout/Core/Sources/Model/Product.swift +++ b/Passepartout/Core/Sources/Model/Product.swift @@ -71,8 +71,6 @@ public struct Product: RawRepresentable, Equatable, Hashable { // MARK: Features #if os(iOS) - public static let unlimitedHosts = Product(featureId: "unlimited_hosts") - public static let trustedNetworks = Product(featureId: "trusted_networks") public static let siriShortcuts = Product(featureId: "siri") @@ -82,7 +80,6 @@ public struct Product: RawRepresentable, Equatable, Hashable { #if os(iOS) public static let allFeatures: [Product] = [ - .unlimitedHosts, .trustedNetworks, .siriShortcuts, .fullVersion diff --git a/Passepartout/Core/Sources/Model/ProductManager.swift b/Passepartout/Core/Sources/Model/ProductManager.swift index de0f9187..47cefd6a 100644 --- a/Passepartout/Core/Sources/Model/ProductManager.swift +++ b/Passepartout/Core/Sources/Model/ProductManager.swift @@ -32,6 +32,10 @@ import TunnelKit private let log = SwiftyBeaver.self +public enum ProductError: Error { + case beta +} + public class ProductManager: NSObject { public struct Configuration { public let isBetaFullVersion: Bool @@ -155,11 +159,17 @@ public class ProductManager: NSObject { return purchasedFeatures.contains(.fullVersion) } - public func isEligible(forFeature feature: Product) -> Bool { + public func isEligible(forFeature feature: Product) throws -> Bool { + guard !isBeta else { + throw ProductError.beta + } return isFullVersion() || purchasedFeatures.contains(feature) } - public func isEligible(forProvider metadata: Infrastructure.Metadata) -> Bool { + public func isEligible(forProvider metadata: Infrastructure.Metadata) throws -> Bool { + guard !isBeta else { + throw ProductError.beta + } return isFullVersion() || purchasedFeatures.contains(metadata.product) }