Move refund detection inside ProductManager (#246)

* Detect refunds inside ProductManager

Compare former value and report refund event via subject.

* Hook VPN uninstallation on refund event
This commit is contained in:
Davide De Rosa 2022-11-06 18:51:13 +01:00 committed by GitHub
parent ba09dcffa7
commit 7ed27558fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 40 additions and 40 deletions

View File

@ -71,6 +71,15 @@ class AppContext {
self.reviewer.reportEvent() self.reviewer.reportEvent()
} }
}.store(in: &cancellables) }.store(in: &cancellables)
productManager.didRefundProducts
.receive(on: DispatchQueue.main)
.sink {
Task {
pp_log.info("Refunds detected, uninstalling VPN profile")
await coreContext.vpnManager.uninstall()
}
}.store(in: &cancellables)
} }
// eligibility: ignore network settings if ineligible // eligibility: ignore network settings if ineligible

View File

@ -27,6 +27,7 @@ import Foundation
import PassepartoutLibrary import PassepartoutLibrary
import StoreKit import StoreKit
import Kvitto import Kvitto
import Combine
enum ProductError: Error { enum ProductError: Error {
case uneligible case uneligible
@ -47,6 +48,8 @@ class ProductManager: NSObject, ObservableObject {
let buildProducts: BuildProducts let buildProducts: BuildProducts
let didRefundProducts = PassthroughSubject<Void, Never>()
@Published private(set) var isRefreshingProducts = false @Published private(set) var isRefreshingProducts = false
@Published private(set) var products: [SKProduct] @Published private(set) var products: [SKProduct]
@ -61,9 +64,18 @@ class ProductManager: NSObject, ObservableObject {
private var purchaseDates: [LocalProduct: Date] private var purchaseDates: [LocalProduct: Date]
private var cancelledPurchases: Set<LocalProduct> private var cancelledPurchases: Set<LocalProduct>? {
willSet {
private var cancelledPurchasesSnapshot: Set<LocalProduct> guard cancelledPurchases != nil else {
return
}
guard let newCancelledPurchases = newValue, newCancelledPurchases != cancelledPurchases else {
pp_log.debug("No purchase was refunded")
return
}
detectRefunds(newCancelledPurchases)
}
}
private var refreshRequest: SKReceiptRefreshRequest? private var refreshRequest: SKReceiptRefreshRequest?
@ -76,8 +88,7 @@ class ProductManager: NSObject, ObservableObject {
purchasedAppBuild = nil purchasedAppBuild = nil
purchasedFeatures = [] purchasedFeatures = []
purchaseDates = [:] purchaseDates = [:]
cancelledPurchases = [] cancelledPurchases = nil
cancelledPurchasesSnapshot = []
super.init() super.init()
@ -216,10 +227,6 @@ class ProductManager: NSObject, ObservableObject {
purchasedFeatures.contains(product) purchasedFeatures.contains(product)
} }
func isCancelledPurchase(_ product: LocalProduct) -> Bool {
cancelledPurchases.contains(product)
}
func purchaseDate(forProduct product: LocalProduct) -> Date? { func purchaseDate(forProduct product: LocalProduct) -> Date? {
purchaseDates[product] purchaseDates[product]
} }
@ -238,7 +245,7 @@ class ProductManager: NSObject, ObservableObject {
purchasedAppBuild = buildNumber purchasedAppBuild = buildNumber
} }
purchasedFeatures.removeAll() purchasedFeatures.removeAll()
cancelledPurchases.removeAll() var newCancelledPurchases: Set<LocalProduct> = []
if let buildNumber = purchasedAppBuild { if let buildNumber = purchasedAppBuild {
pp_log.debug("Original purchased build: \(buildNumber)") pp_log.debug("Original purchased build: \(buildNumber)")
@ -258,7 +265,7 @@ class ProductManager: NSObject, ObservableObject {
} }
if let cancellationDate = $0.cancellationDate { if let cancellationDate = $0.cancellationDate {
pp_log.debug("\t\(pid) [cancelled on: \(cancellationDate)]") pp_log.debug("\t\(pid) [cancelled on: \(cancellationDate)]")
cancelledPurchases.insert(product) newCancelledPurchases.insert(product)
return return
} }
if let purchaseDate = $0.originalPurchaseDate { if let purchaseDate = $0.originalPurchaseDate {
@ -272,6 +279,7 @@ class ProductManager: NSObject, ObservableObject {
if andNotify { if andNotify {
objectWillChange.send() objectWillChange.send()
} }
cancelledPurchases = newCancelledPurchases
} }
} }
@ -284,31 +292,27 @@ extension ProductManager: SKPaymentTransactionObserver {
} }
extension ProductManager { extension ProductManager {
func snapshotRefunds() { private func detectRefunds(_ refunds: Set<LocalProduct>) {
cancelledPurchasesSnapshot = cancelledPurchases
}
func hasNewRefunds() -> Bool {
reloadReceipt(andNotify: false)
guard cancelledPurchases != cancelledPurchasesSnapshot else {
pp_log.debug("No purchase was refunded")
return false
}
let isEligibleForFullVersion = isFullVersion() let isEligibleForFullVersion = isFullVersion()
let hasCancelledFullVersion: Bool let hasCancelledFullVersion: Bool
let hasCancelledTrustedNetworks: Bool let hasCancelledTrustedNetworks: Bool
if isMac { if isMac {
hasCancelledFullVersion = !isEligibleForFullVersion && (isCancelledPurchase(.fullVersion) || isCancelledPurchase(.fullVersion_macOS)) hasCancelledFullVersion = !isEligibleForFullVersion && (
refunds.contains(.fullVersion) || refunds.contains(.fullVersion_macOS)
)
hasCancelledTrustedNetworks = false hasCancelledTrustedNetworks = false
} else { } else {
hasCancelledFullVersion = !isEligibleForFullVersion && (isCancelledPurchase(.fullVersion) || isCancelledPurchase(.fullVersion_iOS)) hasCancelledFullVersion = !isEligibleForFullVersion && (
hasCancelledTrustedNetworks = !isEligibleForFullVersion && isCancelledPurchase(.trustedNetworks) refunds.contains(.fullVersion) || refunds.contains(.fullVersion_iOS)
)
hasCancelledTrustedNetworks = !isEligibleForFullVersion && refunds.contains(.trustedNetworks)
} }
// review features and potentially revert them if they were used (Siri is handled in AppDelegate) // review features and potentially revert them if they were used (Siri is handled in AppDelegate)
return hasCancelledFullVersion || hasCancelledTrustedNetworks if hasCancelledFullVersion || hasCancelledTrustedNetworks {
didRefundProducts.send()
}
} }
} }

View File

@ -34,8 +34,6 @@ extension OrganizerView {
@ObservedObject private var vpnManager: VPNManager @ObservedObject private var vpnManager: VPNManager
@ObservedObject private var productManager: ProductManager
@Binding private var alertType: AlertType? @Binding private var alertType: AlertType?
@Binding private var didHandleSubreddit: Bool @Binding private var didHandleSubreddit: Bool
@ -45,7 +43,6 @@ extension OrganizerView {
init(alertType: Binding<AlertType?>, didHandleSubreddit: Binding<Bool>) { init(alertType: Binding<AlertType?>, didHandleSubreddit: Binding<Bool>) {
profileManager = .shared profileManager = .shared
vpnManager = .shared vpnManager = .shared
productManager = .shared
_alertType = alertType _alertType = alertType
_didHandleSubreddit = didHandleSubreddit _didHandleSubreddit = didHandleSubreddit
} }
@ -85,16 +82,6 @@ extension OrganizerView {
private func onScenePhase(_ phase: ScenePhase) { private func onScenePhase(_ phase: ScenePhase) {
switch phase { switch phase {
case .inactive:
productManager.snapshotRefunds()
case .active:
if productManager.hasNewRefunds() {
Task { @MainActor in
await vpnManager.uninstall()
}
}
case .background: case .background:
persist() persist()
#if targetEnvironment(macCatalyst) #if targetEnvironment(macCatalyst)