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:))