From 4c4876c5f7758913d9d8fa96ce05f1aa0dc9f93d Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Thu, 21 Dec 2023 08:54:00 +0100 Subject: [PATCH] Incorrect handling of receipt purchases (#439) --- Passepartout/App/Info.plist | 2 +- Passepartout/Launcher/Info.plist | 2 +- Passepartout/Mac/Info.plist | 2 +- Passepartout/Tunnel/Info.plist | 2 +- .../Domain/LocalProduct+InApp.swift | 33 ++++++++++ .../Managers/ProductManager.swift | 62 +++++++++++-------- .../ProductManagerTests.swift | 2 + 7 files changed, 75 insertions(+), 30 deletions(-) create mode 100644 PassepartoutLibrary/Sources/PassepartoutFrontend/Domain/LocalProduct+InApp.swift diff --git a/Passepartout/App/Info.plist b/Passepartout/App/Info.plist index 1b21b9cf..2e835550 100644 --- a/Passepartout/App/Info.plist +++ b/Passepartout/App/Info.plist @@ -32,7 +32,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2.2.1 + 2.3.0 CFBundleVersion 3548 ITSAppUsesNonExemptEncryption diff --git a/Passepartout/Launcher/Info.plist b/Passepartout/Launcher/Info.plist index 20a73be3..ecb96ee0 100644 --- a/Passepartout/Launcher/Info.plist +++ b/Passepartout/Launcher/Info.plist @@ -3,7 +3,7 @@ CFBundleShortVersionString - 2.2.1 + 2.3.0 CFBundleVersion 3548 LSBackgroundOnly diff --git a/Passepartout/Mac/Info.plist b/Passepartout/Mac/Info.plist index 985439ea..82901043 100644 --- a/Passepartout/Mac/Info.plist +++ b/Passepartout/Mac/Info.plist @@ -5,7 +5,7 @@ CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleShortVersionString - 2.2.1 + 2.3.0 CFBundleVersion 3548 NSPrincipalClass diff --git a/Passepartout/Tunnel/Info.plist b/Passepartout/Tunnel/Info.plist index fb8ca3b3..a6d8988d 100644 --- a/Passepartout/Tunnel/Info.plist +++ b/Passepartout/Tunnel/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 2.2.1 + 2.3.0 CFBundleVersion 3548 LSMinimumSystemVersion diff --git a/PassepartoutLibrary/Sources/PassepartoutFrontend/Domain/LocalProduct+InApp.swift b/PassepartoutLibrary/Sources/PassepartoutFrontend/Domain/LocalProduct+InApp.swift new file mode 100644 index 00000000..e2ecf426 --- /dev/null +++ b/PassepartoutLibrary/Sources/PassepartoutFrontend/Domain/LocalProduct+InApp.swift @@ -0,0 +1,33 @@ +// +// LocalProduct+InApp.swift +// Passepartout +// +// Created by Davide De Rosa on 12/21/23. +// Copyright (c) 2023 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 . +// + +import Foundation +import PassepartoutCore + +public protocol LocalInApp: InAppProtocol where ProductIdentifier == LocalProduct { +} + +extension StoreKitInApp: LocalInApp where ProductIdentifier == LocalProduct { +} diff --git a/PassepartoutLibrary/Sources/PassepartoutFrontend/Managers/ProductManager.swift b/PassepartoutLibrary/Sources/PassepartoutFrontend/Managers/ProductManager.swift index d7347da3..17e5a62b 100644 --- a/PassepartoutLibrary/Sources/PassepartoutFrontend/Managers/ProductManager.swift +++ b/PassepartoutLibrary/Sources/PassepartoutFrontend/Managers/ProductManager.swift @@ -28,12 +28,6 @@ import Foundation import PassepartoutCore import PassepartoutProviders -public protocol LocalInApp: InAppProtocol where ProductIdentifier == LocalProduct { -} - -extension StoreKitInApp: LocalInApp where ProductIdentifier == LocalProduct { -} - @MainActor public final class ProductManager: NSObject, ObservableObject { private let inApp: any LocalInApp @@ -106,6 +100,8 @@ public final class ProductManager: NSObject, ObservableObject { } } + // MARK: Main interface + public func canMakePayments() -> Bool { inApp.canMakePurchases() } @@ -180,25 +176,19 @@ public final class ProductManager: NSObject, ObservableObject { public func restorePurchases() async throws { try await inApp.restorePurchases() } + + public func hasPurchased(_ product: LocalProduct) -> Bool { + isActivePurchase(product) + } + + public func purchaseDate(forProduct product: LocalProduct) -> Date? { + purchaseDates[product] + } } // MARK: In-app eligibility extension ProductManager { - public func isCurrentPlatformVersion() -> Bool { - purchasedFeatures.contains(isMac ? .fullVersion_macOS : .fullVersion_iOS) - } - - public func isFullVersion() -> Bool { - if appType == .fullVersion { - return true - } - if isCurrentPlatformVersion() { - return true - } - return purchasedFeatures.contains(.fullVersion) - } - public func isEligible(forFeature feature: LocalProduct) -> Bool { if let purchasedAppBuild = purchasedAppBuild { if feature == .networkSettings && buildProducts.hasProduct(.networkSettings, atBuild: purchasedAppBuild) { @@ -206,9 +196,9 @@ extension ProductManager { } } if feature.isPlatformVersion { - return purchasedFeatures.contains(feature) + return isActivePurchase(feature) } - return isFullVersion() || purchasedFeatures.contains(feature) + return isFullVersion() || isActivePurchase(feature) } public func isEligible(forProvider providerName: ProviderName) -> Bool { @@ -221,16 +211,34 @@ extension ProductManager { public func isEligibleForFeedback() -> Bool { appType == .beta || !purchasedFeatures.isEmpty } +} - public func hasPurchased(_ product: LocalProduct) -> Bool { - purchasedFeatures.contains(product) +extension ProductManager { + func isActivePurchase(_ feature: LocalProduct) -> Bool { + purchasedFeatures.contains(feature) && cancelledPurchases?.contains(feature) == false } - public func purchaseDate(forProduct product: LocalProduct) -> Date? { - purchaseDates[product] + func isActivePurchase(where predicate: (LocalProduct) -> Bool) -> Bool { + purchasedFeatures.contains(where: predicate) && cancelledPurchases?.contains(where: predicate) == false + } + + func isCurrentPlatformVersion() -> Bool { + isActivePurchase(isMac ? .fullVersion_macOS : .fullVersion_iOS) + } + + func isFullVersion() -> Bool { + if appType == .fullVersion { + return true + } + if isCurrentPlatformVersion() { + return true + } + return isActivePurchase(.fullVersion) } } +// MARK: Receipt + extension ProductManager { public func reloadReceipt(andNotify: Bool = true) { guard let receipt = receiptReader.receipt(for: appType) else { @@ -280,6 +288,8 @@ extension ProductManager { } } +// MARK: Helpers + private extension ProductManager { var isMac: Bool { #if targetEnvironment(macCatalyst) diff --git a/PassepartoutLibrary/Tests/PassepartoutFrontendTests/ProductManagerTests.swift b/PassepartoutLibrary/Tests/PassepartoutFrontendTests/ProductManagerTests.swift index 2c0eab8b..d0724851 100644 --- a/PassepartoutLibrary/Tests/PassepartoutFrontendTests/ProductManagerTests.swift +++ b/PassepartoutLibrary/Tests/PassepartoutFrontendTests/ProductManagerTests.swift @@ -120,6 +120,7 @@ final class ProductManagerTests: XCTestCase { reader.setReceipt(withBuild: 1500, products: [.fullVersion]) let sut = ProductManager(inApp: inApp, receiptReader: reader, buildProducts: noBuildProducts) + XCTAssertTrue(sut.isFullVersion()) XCTAssertTrue(LocalProduct .allFeatures .filter { !$0.isPlatformVersion } @@ -132,6 +133,7 @@ final class ProductManagerTests: XCTestCase { reader.setReceipt(withBuild: 1500, products: []) let sut = ProductManager(inApp: inApp, receiptReader: reader, buildProducts: noBuildProducts) + XCTAssertFalse(sut.isFullVersion()) XCTAssertFalse(LocalProduct .allFeatures .allSatisfy(sut.isEligible(forFeature:))