Share ProductManager in Core with conditionals

In order to share purchase review logic.

Refactor verification of product eligibility
This commit is contained in:
Davide De Rosa 2021-02-02 17:51:09 +01:00
parent 21e9f5c8cc
commit 7d2ece0256
11 changed files with 187 additions and 285 deletions

View File

@ -25,7 +25,6 @@
0E294AA225AE2B0B00CB4908 /* Descriptible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E294A8225AE29D100CB4908 /* Descriptible.swift */; };
0E2AC24522EC3AC10037B4B0 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 0E2AC24422EC3AC10037B4B0 /* Settings.bundle */; };
0E2B494020FCFF990094784C /* Theme+Titles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2B493F20FCFF990094784C /* Theme+Titles.swift */; };
0E2EB063236D8E1E0079DB53 /* AppConstants+App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2EB062236D8E1E0079DB53 /* AppConstants+App.swift */; };
0E3152AD223F9EF500F61841 /* PassepartoutCore.h in Headers */ = {isa = PBXBuildFile; fileRef = 0E31529D223F9EF500F61841 /* PassepartoutCore.h */; settings = {ATTRIBUTES = (Public, ); }; };
0E3152B0223F9EF500F61841 /* PassepartoutCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0E31529B223F9EF400F61841 /* PassepartoutCore.framework */; };
0E3152B1223F9EF500F61841 /* PassepartoutCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 0E31529B223F9EF400F61841 /* PassepartoutCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
@ -104,12 +103,10 @@
0E52037D259F593B00CBAB56 /* SwiftGen+Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E569F9C259F41690022DFB8 /* SwiftGen+Strings.swift */; };
0E52037E259F593B00CBAB56 /* SwiftGen+Segues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E569FA5259F41690022DFB8 /* SwiftGen+Segues.swift */; };
0E52037F259F593B00CBAB56 /* Theme+Views.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E569F9A259F41690022DFB8 /* Theme+Views.swift */; };
0E520380259F593B00CBAB56 /* AppConstants+App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E569FA3259F41690022DFB8 /* AppConstants+App.swift */; };
0E520381259F593B00CBAB56 /* NSTextView+Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E569F94259F41690022DFB8 /* NSTextView+Search.swift */; };
0E520382259F593B00CBAB56 /* SwiftGen+Scenes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E569F9D259F41690022DFB8 /* SwiftGen+Scenes.swift */; };
0E520383259F593B00CBAB56 /* SwiftGen+Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E569F98259F41690022DFB8 /* SwiftGen+Assets.swift */; };
0E520385259F593B00CBAB56 /* Credits.html in Resources */ = {isa = PBXBuildFile; fileRef = 0E569FA4259F41690022DFB8 /* Credits.html */; };
0E520386259F593B00CBAB56 /* ProductManager+App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E569F9E259F41690022DFB8 /* ProductManager+App.swift */; };
0E520387259F593B00CBAB56 /* TextInputViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E569F97259F41690022DFB8 /* TextInputViewController.swift */; };
0E520388259F593B00CBAB56 /* HostImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E569F93259F41690022DFB8 /* HostImporter.swift */; };
0E52038F259F593F00CBAB56 /* App.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0E569F8D259F41690022DFB8 /* App.strings */; };
@ -207,7 +204,6 @@
0EB60FDA2111136E00AD27F3 /* UITextView+Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EB60FD92111136E00AD27F3 /* UITextView+Search.swift */; };
0EB67D6B2184581E00BA6200 /* ImportedHostsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EB67D6A2184581E00BA6200 /* ImportedHostsViewController.swift */; };
0EBE3A79213C4E5500BFA2F5 /* OrganizerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBE3A78213C4E5400BFA2F5 /* OrganizerViewController.swift */; };
0ECA7E2225967BB90095F369 /* ProductManager+App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECA7E2125967BB90095F369 /* ProductManager+App.swift */; };
0ECC60DE2256B68A0020BEAC /* SwiftGen+Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECC60DD2256B6890020BEAC /* SwiftGen+Assets.swift */; };
0ECEB10A224FECEA00E9E551 /* DataUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECEB109224FECEA00E9E551 /* DataUnit.swift */; };
0ECEE45020E1182E00A6BB43 /* Theme+Cells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECEE44F20E1182E00A6BB43 /* Theme+Cells.swift */; };
@ -376,7 +372,6 @@
0E2C54C4230056EF00F59453 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Core.strings"; sourceTree = "<group>"; };
0E2C54C52300570200F59453 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/App.strings"; sourceTree = "<group>"; };
0E2D11B9217DBEDE0096822C /* ConnectionService+Configurations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConnectionService+Configurations.swift"; sourceTree = "<group>"; };
0E2EB062236D8E1E0079DB53 /* AppConstants+App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppConstants+App.swift"; sourceTree = "<group>"; };
0E31529B223F9EF400F61841 /* PassepartoutCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PassepartoutCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
0E31529D223F9EF500F61841 /* PassepartoutCore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PassepartoutCore.h; sourceTree = "<group>"; };
0E31529E223F9EF500F61841 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -474,9 +469,7 @@
0E569F9A259F41690022DFB8 /* Theme+Views.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Theme+Views.swift"; sourceTree = "<group>"; };
0E569F9C259F41690022DFB8 /* SwiftGen+Strings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SwiftGen+Strings.swift"; sourceTree = "<group>"; };
0E569F9D259F41690022DFB8 /* SwiftGen+Scenes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SwiftGen+Scenes.swift"; sourceTree = "<group>"; };
0E569F9E259F41690022DFB8 /* ProductManager+App.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ProductManager+App.swift"; sourceTree = "<group>"; };
0E569FA1259F41690022DFB8 /* Theme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; };
0E569FA3259F41690022DFB8 /* AppConstants+App.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AppConstants+App.swift"; sourceTree = "<group>"; };
0E569FA4259F41690022DFB8 /* Credits.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = Credits.html; sourceTree = "<group>"; };
0E569FA5259F41690022DFB8 /* SwiftGen+Segues.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SwiftGen+Segues.swift"; sourceTree = "<group>"; };
0E569FA6259F41690022DFB8 /* Macros.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Macros.swift; sourceTree = "<group>"; };
@ -544,7 +537,6 @@
0EBE3AA4213DC1B000BFA2F5 /* ProviderConnectionProfile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProviderConnectionProfile.swift; sourceTree = "<group>"; };
0EBE8D2E25C076F900798607 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/App.strings; sourceTree = "<group>"; };
0EC7F20420E24308004EA58E /* DebugLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugLog.swift; sourceTree = "<group>"; };
0ECA7E2125967BB90095F369 /* ProductManager+App.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ProductManager+App.swift"; sourceTree = "<group>"; };
0ECC60DD2256B6890020BEAC /* SwiftGen+Assets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SwiftGen+Assets.swift"; sourceTree = "<group>"; };
0ECEB105224FE51400E9E551 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
0ECEB106224FE51400E9E551 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Organizer.storyboard; sourceTree = "<group>"; };
@ -871,12 +863,10 @@
isa = PBXGroup;
children = (
0E569FA4259F41690022DFB8 /* Credits.html */,
0E569FA3259F41690022DFB8 /* AppConstants+App.swift */,
0E569F93259F41690022DFB8 /* HostImporter.swift */,
0E569F8F259F41690022DFB8 /* IssueReporter.swift */,
0E569FA6259F41690022DFB8 /* Macros.swift */,
0E569F94259F41690022DFB8 /* NSTextView+Search.swift */,
0E569F9E259F41690022DFB8 /* ProductManager+App.swift */,
0E569F98259F41690022DFB8 /* SwiftGen+Assets.swift */,
0E569F9D259F41690022DFB8 /* SwiftGen+Scenes.swift */,
0E569FA5259F41690022DFB8 /* SwiftGen+Segues.swift */,
@ -1062,11 +1052,9 @@
0EDE8DEC20C93E3B004C739C /* Global */ = {
isa = PBXGroup;
children = (
0E2EB062236D8E1E0079DB53 /* AppConstants+App.swift */,
0E3262D8235EE8DA00B5E470 /* HostImporter.swift */,
0EFD943D215BE10800529B64 /* IssueReporter.swift */,
0E4FD7F020D58618002221FF /* Macros.swift */,
0ECA7E2125967BB90095F369 /* ProductManager+App.swift */,
0ECC60DD2256B6890020BEAC /* SwiftGen+Assets.swift */,
0EDE8DE320C89028004C739C /* SwiftGen+Scenes.swift */,
0EF56BBA2185AC8500B0C8AB /* SwiftGen+Segues.swift */,
@ -1909,12 +1897,10 @@
0E520346259F58FE00CBAB56 /* TrustedNetworksViewController.swift in Sources */,
0E520338259F58F500CBAB56 /* ProviderServiceView.swift in Sources */,
0E520347259F58FE00CBAB56 /* ProxyViewController.swift in Sources */,
0E520380259F593B00CBAB56 /* AppConstants+App.swift in Sources */,
0E52037B259F593B00CBAB56 /* IssueReporter.swift in Sources */,
0E520335259F58F500CBAB56 /* HostServiceView.swift in Sources */,
0E520333259F58F500CBAB56 /* OrganizerProfileTableView.swift in Sources */,
0E52037F259F593B00CBAB56 /* Theme+Views.swift in Sources */,
0E520386259F593B00CBAB56 /* ProductManager+App.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -2002,7 +1988,6 @@
0ED31C2C20CF2D6F0027975F /* ProviderPoolViewController.swift in Sources */,
0E2B494020FCFF990094784C /* Theme+Titles.swift in Sources */,
0E05C5D520D1645F006EE732 /* SettingTableViewCell.swift in Sources */,
0E2EB063236D8E1E0079DB53 /* AppConstants+App.swift in Sources */,
0E89DFCE213EEDFA00741BA1 /* WizardProviderViewController.swift in Sources */,
0E1D72B2213BFFCF00BA1586 /* ProviderPresetViewController.swift in Sources */,
0E6BE13F20CFBAB300A6DD36 /* DebugLogViewController.swift in Sources */,
@ -2013,7 +1998,6 @@
0E57F63E20C83FC5008323CF /* ServiceViewController.swift in Sources */,
0E36D24D2240234B006AF062 /* ShortcutsAddViewController.swift in Sources */,
0E57F63C20C83FC5008323CF /* AppDelegate.swift in Sources */,
0ECA7E2225967BB90095F369 /* ProductManager+App.swift in Sources */,
0ED31C2920CF2A340027975F /* AccountViewController.swift in Sources */,
0E158ADA20E11B0B00C85A82 /* EndpointViewController.swift in Sources */,
0E1D72B4213C118500BA1586 /* ConfigurationViewController.swift in Sources */,

View File

@ -162,7 +162,9 @@ extension UISplitViewController {
extension AppDelegate {
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
guard (try? ProductManager.shared.isEligible(forFeature: .siriShortcuts)) ?? false else {
do {
try ProductManager.shared.verifyEligible(forFeature: .siriShortcuts)
} catch {
return false
}
guard let interaction = userActivity.interaction else {

View File

@ -1,41 +0,0 @@
//
// AppConstants+App.swift
// Passepartout
//
// Created by Davide De Rosa on 11/2/19.
// Copyright (c) 2021 Davide De Rosa. All rights reserved.
//
// https://github.com/passepartoutvpn
//
// This file is part of Passepartout.
//
// Passepartout is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Passepartout is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
import Foundation
import PassepartoutCore
extension AppConstants {
struct Rating {
static let eventCount = 3
}
struct InApp {
static var isBetaFullVersion: Bool {
return ProcessInfo.processInfo.environment["FULL_VERSION"] != nil
}
static let lastFullVersionBuild = 2016
}
}

View File

@ -1,99 +0,0 @@
//
// ProductManager+App.swift
// Passepartout
//
// Created by Davide De Rosa on 4/6/19.
// Copyright (c) 2021 Davide De Rosa. All rights reserved.
//
// https://github.com/passepartoutvpn
//
// This file is part of Passepartout.
//
// Passepartout is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Passepartout is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
import Foundation
import PassepartoutCore
import TunnelKit
import SwiftyBeaver
private let log = SwiftyBeaver.self
extension ProductManager {
static let shared = ProductManager(
Configuration(
isBetaFullVersion: AppConstants.InApp.isBetaFullVersion,
lastFullVersionBuild: AppConstants.InApp.lastFullVersionBuild
)
)
public func reviewPurchases() {
let service = TransientStore.shared.service
reloadReceipt(andNotify: false)
let isFullVersion = (try? isEligible(forFeature: .fullVersion)) ?? false
var anyRefund = false
// review features and potentially revert them if they were used (Siri is handled in AppDelegate)
log.debug("Checking 'Trusted networks'")
if isCancelledPurchase(.fullVersion) || (!isFullVersion && isCancelledPurchase(.trustedNetworks)) {
// reset trusted networks for ALL profiles (must load first)
for key in service.allProfileKeys() {
guard let profile = service.profile(withKey: key) else {
continue
}
#if os(iOS)
if profile.trustedNetworks.includesMobile || !profile.trustedNetworks.includedWiFis.isEmpty {
profile.trustedNetworks.includesMobile = false
profile.trustedNetworks.includedWiFis.removeAll()
anyRefund = true
}
#else
if !profile.trustedNetworks.includedWiFis.isEmpty {
profile.trustedNetworks.includedWiFis.removeAll()
anyRefund = true
}
#endif
}
if anyRefund {
log.debug("\tRefunded")
}
}
log.debug("Checking providers")
for name in service.providerNames() {
guard let metadata = InfrastructureFactory.shared.metadata(forName: name) else {
continue
}
if isCancelledPurchase(.fullVersion) || (!isFullVersion && isCancelledPurchase(metadata.product)) {
service.removeProfile(ProfileKey(name))
log.debug("\tRefunded provider: \(name)")
anyRefund = true
}
}
guard anyRefund else {
return
}
//
// save reverts and remove fraud VPN profile
TransientStore.shared.serialize(withProfiles: true)
VPN.shared.uninstall(completionHandler: nil)
NotificationCenter.default.post(name: ProductManager.didReviewPurchases, object: nil)
}
}

View File

@ -242,13 +242,13 @@ class OrganizerViewController: UITableViewController, StrongTableHost {
private func addShortcuts() {
do {
guard try ProductManager.shared.isEligible(forFeature: .siriShortcuts) else {
presentPurchaseScreen(forProduct: .siriShortcuts)
return
}
} catch {
try ProductManager.shared.verifyEligible(forFeature: .siriShortcuts)
} catch ProductError.beta {
presentBetaFeatureUnavailable("Siri")
return
} catch {
presentPurchaseScreen(forProduct: .siriShortcuts)
return
}
perform(segue: StoryboardSegue.Organizer.siriShortcutsSegueIdentifier)
}

View File

@ -71,15 +71,15 @@ class WizardProviderViewController: UITableViewController, StrongTableHost {
selectedMetadata = metadata
do {
guard try ProductManager.shared.isEligible(forProvider: metadata) else {
guard purchaseIfNecessary else {
return
}
presentPurchaseScreen(forProduct: metadata.product, delegate: self)
try ProductManager.shared.verifyEligible(forProvider: metadata)
} catch ProductError.beta {
presentBetaFeatureUnavailable("Providers")
return
} catch {
guard purchaseIfNecessary else {
return
}
} catch {
presentBetaFeatureUnavailable("Providers")
presentPurchaseScreen(forProduct: metadata.product, delegate: self)
return
}

View File

@ -378,19 +378,19 @@ class ServiceViewController: UIViewController, StrongTableHost {
private func trustMobileNetwork(cell: ToggleTableViewCell) {
do {
guard try ProductManager.shared.isEligible(forFeature: .trustedNetworks) else {
delay {
cell.setOn(false, animated: true)
}
presentPurchaseScreen(forProduct: .trustedNetworks)
return
}
} catch {
try ProductManager.shared.verifyEligible(forFeature: .trustedNetworks)
} catch ProductError.beta {
delay {
cell.setOn(false, animated: true)
}
presentBetaFeatureUnavailable("Trusted networks")
return
} catch {
delay {
cell.setOn(false, animated: true)
}
presentPurchaseScreen(forProduct: .trustedNetworks)
return
}
if #available(iOS 12, *) {
@ -403,13 +403,13 @@ class ServiceViewController: UIViewController, StrongTableHost {
private func trustCurrentWiFi() {
do {
guard try ProductManager.shared.isEligible(forFeature: .trustedNetworks) else {
presentPurchaseScreen(forProduct: .trustedNetworks)
return
}
} catch {
try ProductManager.shared.verifyEligible(forFeature: .trustedNetworks)
} catch ProductError.beta {
presentBetaFeatureUnavailable("Trusted networks")
return
} catch {
presentPurchaseScreen(forProduct: .trustedNetworks)
return
}
if #available(iOS 13, *) {
@ -461,19 +461,19 @@ class ServiceViewController: UIViewController, StrongTableHost {
private func toggleTrustWiFi(cell: ToggleTableViewCell, at row: Int) {
do {
guard try ProductManager.shared.isEligible(forFeature: .trustedNetworks) else {
delay {
cell.setOn(false, animated: true)
}
presentPurchaseScreen(forProduct: .trustedNetworks)
return
}
} catch {
try ProductManager.shared.verifyEligible(forFeature: .trustedNetworks)
} catch ProductError.beta {
delay {
cell.setOn(false, animated: true)
}
presentBetaFeatureUnavailable("Trusted networks")
return
} catch {
delay {
cell.setOn(false, animated: true)
}
presentPurchaseScreen(forProduct: .trustedNetworks)
return
}
if cell.isOn {

View File

@ -1,46 +0,0 @@
//
// AppConstants+App.swift
// Passepartout
//
// Created by Davide De Rosa on 11/4/19.
// Copyright (c) 2021 Davide De Rosa. All rights reserved.
//
// https://github.com/passepartoutvpn
//
// This file is part of Passepartout.
//
// Passepartout is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Passepartout is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
import Foundation
import PassepartoutCore
extension AppConstants {
struct Rating {
static let eventCount = 10
}
struct InApp {
static var isBetaFullVersion: Bool {
guard !ProcessInfo.processInfo.arguments.contains("FULL_VERSION") else {
return true
}
return false
}
static let lastFullVersionBuild = 0
static let limitedNumberOfHosts = 2
}
}

View File

@ -1,36 +0,0 @@
//
// ProductManager+App.swift
// Passepartout
//
// Created by Davide De Rosa on 12/25/20.
// Copyright (c) 2021 Davide De Rosa. All rights reserved.
//
// https://github.com/passepartoutvpn
//
// This file is part of Passepartout.
//
// Passepartout is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Passepartout is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
import Foundation
import PassepartoutCore
extension ProductManager {
static let shared = ProductManager(
Configuration(
isBetaFullVersion: AppConstants.InApp.isBetaFullVersion,
lastFullVersionBuild: AppConstants.InApp.lastFullVersionBuild
)
)
}

View File

@ -342,4 +342,31 @@ public class AppConstants {
)
]
}
public struct Rating {
#if os(iOS)
public static let eventCount = 3
#else
public static let eventCount = 10
#endif
}
struct InApp {
#if os(iOS)
static var isBetaFullVersion: Bool {
return ProcessInfo.processInfo.environment["FULL_VERSION"] != nil
}
static let lastFullVersionBuild = 2016
#else
static var isBetaFullVersion: Bool {
guard !ProcessInfo.processInfo.arguments.contains("FULL_VERSION") else {
return true
}
return false
}
static let lastFullVersionBuild = 0
#endif
}
}

View File

@ -33,6 +33,8 @@ import TunnelKit
private let log = SwiftyBeaver.self
public enum ProductError: Error {
case uneligible
case beta
}
@ -161,25 +163,23 @@ public class ProductManager: NSObject {
#endif
return purchasedFeatures.contains(.fullVersion)
}
public func isEligible(forFeature feature: Product) throws -> Bool {
if isBeta {
guard cfg.isBetaFullVersion else {
throw ProductError.beta
}
}
private func isEligible(forFeature feature: Product) -> Bool {
return isFullVersion() || purchasedFeatures.contains(feature)
}
public func isEligible(forProvider metadata: Infrastructure.Metadata) throws -> Bool {
if isBeta {
guard cfg.isBetaFullVersion else {
throw ProductError.beta
}
}
private func isEligible(forProvider metadata: Infrastructure.Metadata) -> Bool {
return isFullVersion() || purchasedFeatures.contains(metadata.product)
}
private func isEligibleForTrustedNetworks() -> Bool {
#if os(iOS)
return isFullVersion() || purchasedFeatures.contains(.trustedNetworks)
#else
return isFullVersion()
#endif
}
public func isEligibleForFeedback() -> Bool {
#if os(iOS)
return isBeta || !purchasedFeatures.isEmpty
@ -188,6 +188,39 @@ public class ProductManager: NSObject {
#endif
}
public func verifyEligible(forFeature feature: Product) throws {
if isBeta {
guard cfg.isBetaFullVersion else {
throw ProductError.beta
}
}
guard isEligible(forFeature: feature) else {
throw ProductError.uneligible
}
}
public func verifyEligible(forProvider metadata: Infrastructure.Metadata) throws {
if isBeta {
guard cfg.isBetaFullVersion else {
throw ProductError.beta
}
}
guard isFullVersion() || purchasedFeatures.contains(metadata.product) else {
throw ProductError.uneligible
}
}
public func verifyEligibleForTrustedNetworks() throws {
if isBeta {
guard cfg.isBetaFullVersion else {
throw ProductError.beta
}
}
guard isEligibleForTrustedNetworks() else {
throw ProductError.uneligible
}
}
public func isCancelledPurchase(_ product: Product) -> Bool {
return cancelledPurchases.contains(product)
}
@ -271,3 +304,81 @@ extension ProductManager: SKRequestDelegate {
restoreCompletionHandler = nil
}
}
extension ProductManager {
public static let shared = ProductManager(
Configuration(
isBetaFullVersion: AppConstants.InApp.isBetaFullVersion,
lastFullVersionBuild: AppConstants.InApp.lastFullVersionBuild
)
)
public func reviewPurchases() {
let service = TransientStore.shared.service
reloadReceipt(andNotify: false)
let isEligibleForFullVersion = isFullVersion()
let hasCancelledFullVersion: Bool
let hasCancelledTrustedNetworks: Bool
var anyRefund = false
#if os(iOS)
hasCancelledFullVersion = !isEligibleForFullVersion && (isCancelledPurchase(.fullVersion) || isCancelledPurchase(.fullVersion_iOS))
hasCancelledTrustedNetworks = !isEligibleForFullVersion && isCancelledPurchase(.trustedNetworks)
#else
hasCancelledFullVersion = !isEligibleForFullVersion && (isCancelledPurchase(.fullVersion) || isCancelledPurchase(.fullVersion_macOS))
hasCancelledTrustedNetworks = false
#endif
// review features and potentially revert them if they were used (Siri is handled in AppDelegate)
log.debug("Checking 'Trusted networks'")
if hasCancelledFullVersion || hasCancelledTrustedNetworks {
// reset trusted networks for ALL profiles (must load first)
for key in service.allProfileKeys() {
guard let profile = service.profile(withKey: key) else {
continue
}
#if os(iOS)
if profile.trustedNetworks.includesMobile || !profile.trustedNetworks.includedWiFis.isEmpty {
profile.trustedNetworks.includesMobile = false
profile.trustedNetworks.includedWiFis.removeAll()
anyRefund = true
}
#else
if !profile.trustedNetworks.includedWiFis.isEmpty {
profile.trustedNetworks.includedWiFis.removeAll()
anyRefund = true
}
#endif
}
if anyRefund {
log.debug("\tRefunded")
}
}
log.debug("Checking providers")
for name in service.providerNames() {
guard let metadata = InfrastructureFactory.shared.metadata(forName: name) else {
continue
}
if hasCancelledFullVersion || (!isEligibleForFullVersion && isCancelledPurchase(metadata.product)) {
service.removeProfile(ProfileKey(name))
log.debug("\tRefunded provider: \(name)")
anyRefund = true
}
}
guard anyRefund else {
return
}
//
// save reverts and remove fraud VPN profile
TransientStore.shared.serialize(withProfiles: true)
VPN.shared.uninstall(completionHandler: nil)
NotificationCenter.default.post(name: ProductManager.didReviewPurchases, object: nil)
}
}