diff --git a/Passepartout/App/Context/AppContext.swift b/Passepartout/App/Context/AppContext.swift index 119e047f..aef259c4 100644 --- a/Passepartout/App/Context/AppContext.swift +++ b/Passepartout/App/Context/AppContext.swift @@ -70,7 +70,16 @@ class AppContext { pp_log.info("VPN successful connection, report to Reviewer") 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 diff --git a/Passepartout/App/InApp/ProductManager.swift b/Passepartout/App/InApp/ProductManager.swift index fc34cb6e..6d3e1ea0 100644 --- a/Passepartout/App/InApp/ProductManager.swift +++ b/Passepartout/App/InApp/ProductManager.swift @@ -27,6 +27,7 @@ import Foundation import PassepartoutLibrary import StoreKit import Kvitto +import Combine enum ProductError: Error { case uneligible @@ -47,6 +48,8 @@ class ProductManager: NSObject, ObservableObject { let buildProducts: BuildProducts + let didRefundProducts = PassthroughSubject() + @Published private(set) var isRefreshingProducts = false @Published private(set) var products: [SKProduct] @@ -61,9 +64,18 @@ class ProductManager: NSObject, ObservableObject { private var purchaseDates: [LocalProduct: Date] - private var cancelledPurchases: Set - - private var cancelledPurchasesSnapshot: Set + private var cancelledPurchases: Set? { + willSet { + 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? @@ -76,8 +88,7 @@ class ProductManager: NSObject, ObservableObject { purchasedAppBuild = nil purchasedFeatures = [] purchaseDates = [:] - cancelledPurchases = [] - cancelledPurchasesSnapshot = [] + cancelledPurchases = nil super.init() @@ -216,10 +227,6 @@ class ProductManager: NSObject, ObservableObject { purchasedFeatures.contains(product) } - func isCancelledPurchase(_ product: LocalProduct) -> Bool { - cancelledPurchases.contains(product) - } - func purchaseDate(forProduct product: LocalProduct) -> Date? { purchaseDates[product] } @@ -238,7 +245,7 @@ class ProductManager: NSObject, ObservableObject { purchasedAppBuild = buildNumber } purchasedFeatures.removeAll() - cancelledPurchases.removeAll() + var newCancelledPurchases: Set = [] if let buildNumber = purchasedAppBuild { pp_log.debug("Original purchased build: \(buildNumber)") @@ -258,7 +265,7 @@ class ProductManager: NSObject, ObservableObject { } if let cancellationDate = $0.cancellationDate { pp_log.debug("\t\(pid) [cancelled on: \(cancellationDate)]") - cancelledPurchases.insert(product) + newCancelledPurchases.insert(product) return } if let purchaseDate = $0.originalPurchaseDate { @@ -272,6 +279,7 @@ class ProductManager: NSObject, ObservableObject { if andNotify { objectWillChange.send() } + cancelledPurchases = newCancelledPurchases } } @@ -284,31 +292,27 @@ extension ProductManager: SKPaymentTransactionObserver { } extension ProductManager { - func snapshotRefunds() { - cancelledPurchasesSnapshot = cancelledPurchases - } - - func hasNewRefunds() -> Bool { - reloadReceipt(andNotify: false) - guard cancelledPurchases != cancelledPurchasesSnapshot else { - pp_log.debug("No purchase was refunded") - return false - } - + private func detectRefunds(_ refunds: Set) { let isEligibleForFullVersion = isFullVersion() let hasCancelledFullVersion: Bool let hasCancelledTrustedNetworks: Bool if isMac { - hasCancelledFullVersion = !isEligibleForFullVersion && (isCancelledPurchase(.fullVersion) || isCancelledPurchase(.fullVersion_macOS)) + hasCancelledFullVersion = !isEligibleForFullVersion && ( + refunds.contains(.fullVersion) || refunds.contains(.fullVersion_macOS) + ) hasCancelledTrustedNetworks = false } else { - hasCancelledFullVersion = !isEligibleForFullVersion && (isCancelledPurchase(.fullVersion) || isCancelledPurchase(.fullVersion_iOS)) - hasCancelledTrustedNetworks = !isEligibleForFullVersion && isCancelledPurchase(.trustedNetworks) + hasCancelledFullVersion = !isEligibleForFullVersion && ( + 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) - return hasCancelledFullVersion || hasCancelledTrustedNetworks + if hasCancelledFullVersion || hasCancelledTrustedNetworks { + didRefundProducts.send() + } } } diff --git a/Passepartout/App/Views/OrganizerView+Scene.swift b/Passepartout/App/Views/OrganizerView+Scene.swift index 87b4cb2d..6fd95431 100644 --- a/Passepartout/App/Views/OrganizerView+Scene.swift +++ b/Passepartout/App/Views/OrganizerView+Scene.swift @@ -34,8 +34,6 @@ extension OrganizerView { @ObservedObject private var vpnManager: VPNManager - @ObservedObject private var productManager: ProductManager - @Binding private var alertType: AlertType? @Binding private var didHandleSubreddit: Bool @@ -45,7 +43,6 @@ extension OrganizerView { init(alertType: Binding, didHandleSubreddit: Binding) { profileManager = .shared vpnManager = .shared - productManager = .shared _alertType = alertType _didHandleSubreddit = didHandleSubreddit } @@ -85,16 +82,6 @@ extension OrganizerView { private func onScenePhase(_ phase: ScenePhase) { switch phase { - case .inactive: - productManager.snapshotRefunds() - - case .active: - if productManager.hasNewRefunds() { - Task { @MainActor in - await vpnManager.uninstall() - } - } - case .background: persist() #if targetEnvironment(macCatalyst)