Merge branch 'handle-malicious-refunds'

This commit is contained in:
Davide De Rosa 2019-11-09 18:08:17 +01:00
commit 1ed4d32d5d
4 changed files with 80 additions and 5 deletions

View File

@ -27,6 +27,9 @@ import UIKit
import TunnelKit import TunnelKit
import PassepartoutCore import PassepartoutCore
import Convenience import Convenience
import SwiftyBeaver
private let log = SwiftyBeaver.self
@UIApplicationMain @UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDelegate { class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDelegate {
@ -79,6 +82,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele
func applicationDidBecomeActive(_ application: UIApplication) { func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
ProductManager.shared.reviewPurchases()
} }
func applicationWillTerminate(_ application: UIApplication) { func applicationWillTerminate(_ application: UIApplication) {
@ -161,6 +165,9 @@ extension UISplitViewController {
extension AppDelegate { extension AppDelegate {
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
guard ProductManager.shared.isEligible(forFeature: .siriShortcuts) else {
return false
}
guard let interaction = userActivity.interaction else { guard let interaction = userActivity.interaction else {
return false return false
} }

View File

@ -35,6 +35,8 @@ private let log = SwiftyBeaver.self
class ProductManager: NSObject { class ProductManager: NSObject {
static let didReloadReceipt = Notification.Name("ProductManagerDidReloadReceipt") static let didReloadReceipt = Notification.Name("ProductManagerDidReloadReceipt")
static let didReviewPurchases = Notification.Name("ProductManagerDidReviewPurchases")
private static let lastFullVersionBuild = 2016 // 1.8.1 private static let lastFullVersionBuild = 2016 // 1.8.1
static let shared = ProductManager() static let shared = ProductManager()
@ -96,7 +98,7 @@ class ProductManager: NSObject {
// MARK: In-app eligibility // MARK: In-app eligibility
private func reloadReceipt() { private func reloadReceipt(andNotify: Bool = true) {
guard let url = Bundle.main.appStoreReceiptURL else { guard let url = Bundle.main.appStoreReceiptURL else {
log.warning("No App Store receipt found!") log.warning("No App Store receipt found!")
return return
@ -122,21 +124,27 @@ class ProductManager: NSObject {
if let iapReceipts = receipt.inAppPurchaseReceipts { if let iapReceipts = receipt.inAppPurchaseReceipts {
log.debug("In-app receipts:") log.debug("In-app receipts:")
iapReceipts.forEach { iapReceipts.forEach {
guard let pid = $0.productIdentifier, let date = $0.originalPurchaseDate else { guard let pid = $0.productIdentifier, let purchaseDate = $0.originalPurchaseDate else {
return return
} }
log.debug("\t\(pid) [\(date)]") log.debug("\t\(pid) [purchased on: \(purchaseDate)]")
} }
for r in iapReceipts { for r in iapReceipts {
guard let pid = r.productIdentifier, let product = Product(rawValue: pid) else { guard let pid = r.productIdentifier, let product = Product(rawValue: pid) else {
continue continue
} }
if let cancellationDate = r.cancellationDate {
log.debug("\t\(pid) [cancelled on: \(cancellationDate)]")
continue
}
purchasedFeatures.insert(product) purchasedFeatures.insert(product)
} }
} }
log.info("Purchased features: \(purchasedFeatures)") log.info("Purchased features: \(purchasedFeatures)")
NotificationCenter.default.post(name: ProductManager.didReloadReceipt, object: nil) if andNotify {
NotificationCenter.default.post(name: ProductManager.didReloadReceipt, object: nil)
}
} }
func isFullVersion() -> Bool { func isFullVersion() -> Bool {
@ -165,6 +173,60 @@ class ProductManager: NSObject {
func isEligibleForFeedback() -> Bool { func isEligibleForFeedback() -> Bool {
return AppConstants.Flags.isBeta || !purchasedFeatures.isEmpty return AppConstants.Flags.isBeta || !purchasedFeatures.isEmpty
} }
// MARK: Review
func reviewPurchases() {
let service = TransientStore.shared.service
reloadReceipt(andNotify: false)
var shouldReinstall = false
// 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 service.preferences.trustsMobileNetwork || !service.preferences.trustedWifis.isEmpty {
service.preferences.trustsMobileNetwork = false
service.preferences.trustedWifis.removeAll()
log.debug("\tRefunded")
shouldReinstall = true
}
}
log.debug("Checking 'Unlimited hosts'")
if !isEligible(forFeature: .unlimitedHosts) {
let ids = service.ids(forContext: .host)
if ids.count > AppConstants.InApp.limitedNumberOfHosts {
for id in ids {
service.removeProfile(ProfileKey(.host, id))
}
log.debug("\tRefunded")
shouldReinstall = true
}
}
log.debug("Checking providers")
for name in service.currentProviderNames() {
if !isEligible(forProvider: name) {
service.removeProfile(ProfileKey(name))
log.debug("\tRefunded provider: \(name)")
shouldReinstall = true
}
}
// no refunds
guard shouldReinstall 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)
}
} }
extension ConnectionService { extension ConnectionService {

View File

@ -120,6 +120,7 @@ class ServiceViewController: UIViewController, StrongTableHost {
} }
nc.addObserver(self, selector: #selector(serviceDidUpdateDataCount(_:)), name: ConnectionService.didUpdateDataCount, object: nil) nc.addObserver(self, selector: #selector(serviceDidUpdateDataCount(_:)), name: ConnectionService.didUpdateDataCount, object: nil)
nc.addObserver(self, selector: #selector(productManagerDidReloadReceipt), name: ProductManager.didReloadReceipt, object: nil) nc.addObserver(self, selector: #selector(productManagerDidReloadReceipt), name: ProductManager.didReloadReceipt, object: nil)
nc.addObserver(self, selector: #selector(productManagerDidReviewPurchases), name: ProductManager.didReviewPurchases, object: nil)
// run this no matter what // run this no matter what
// XXX: convenient here vs AppDelegate for updating table // XXX: convenient here vs AppDelegate for updating table
@ -141,6 +142,7 @@ class ServiceViewController: UIViewController, StrongTableHost {
super.viewDidAppear(animated) super.viewDidAppear(animated)
clearSelection() clearSelection()
hideProfileIfDeleted()
} }
override func didReceiveMemoryWarning() { override func didReceiveMemoryWarning() {
@ -674,6 +676,10 @@ class ServiceViewController: UIViewController, StrongTableHost {
reloadModel() reloadModel()
tableView.reloadData() tableView.reloadData()
} }
@objc private func productManagerDidReviewPurchases() {
hideProfileIfDeleted()
}
} }
// MARK: - // MARK: -

@ -1 +1 @@
Subproject commit 7476e0751d1a64efca0180a1e31f0e24f04fce84 Subproject commit dad47f3581ab25d966d204de93634d82b9fe808b