Incorrect handling of receipt purchases (#439)

This commit is contained in:
Davide De Rosa 2023-12-21 08:54:00 +01:00 committed by GitHub
parent 1551b59f21
commit 4c4876c5f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 75 additions and 30 deletions

View File

@ -32,7 +32,7 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>2.2.1</string> <string>2.3.0</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>3548</string> <string>3548</string>
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>

View File

@ -3,7 +3,7 @@
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>2.2.1</string> <string>2.3.0</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>3548</string> <string>3548</string>
<key>LSBackgroundOnly</key> <key>LSBackgroundOnly</key>

View File

@ -5,7 +5,7 @@
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>2.2.1</string> <string>2.3.0</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>3548</string> <string>3548</string>
<key>NSPrincipalClass</key> <key>NSPrincipalClass</key>

View File

@ -17,7 +17,7 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>XPC!</string> <string>XPC!</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>2.2.1</string> <string>2.3.0</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>3548</string> <string>3548</string>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>

View File

@ -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 <http://www.gnu.org/licenses/>.
//
import Foundation
import PassepartoutCore
public protocol LocalInApp: InAppProtocol where ProductIdentifier == LocalProduct {
}
extension StoreKitInApp: LocalInApp where ProductIdentifier == LocalProduct {
}

View File

@ -28,12 +28,6 @@ import Foundation
import PassepartoutCore import PassepartoutCore
import PassepartoutProviders import PassepartoutProviders
public protocol LocalInApp: InAppProtocol where ProductIdentifier == LocalProduct {
}
extension StoreKitInApp: LocalInApp where ProductIdentifier == LocalProduct {
}
@MainActor @MainActor
public final class ProductManager: NSObject, ObservableObject { public final class ProductManager: NSObject, ObservableObject {
private let inApp: any LocalInApp private let inApp: any LocalInApp
@ -106,6 +100,8 @@ public final class ProductManager: NSObject, ObservableObject {
} }
} }
// MARK: Main interface
public func canMakePayments() -> Bool { public func canMakePayments() -> Bool {
inApp.canMakePurchases() inApp.canMakePurchases()
} }
@ -180,25 +176,19 @@ public final class ProductManager: NSObject, ObservableObject {
public func restorePurchases() async throws { public func restorePurchases() async throws {
try await inApp.restorePurchases() 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 // MARK: In-app eligibility
extension ProductManager { 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 { public func isEligible(forFeature feature: LocalProduct) -> Bool {
if let purchasedAppBuild = purchasedAppBuild { if let purchasedAppBuild = purchasedAppBuild {
if feature == .networkSettings && buildProducts.hasProduct(.networkSettings, atBuild: purchasedAppBuild) { if feature == .networkSettings && buildProducts.hasProduct(.networkSettings, atBuild: purchasedAppBuild) {
@ -206,9 +196,9 @@ extension ProductManager {
} }
} }
if feature.isPlatformVersion { 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 { public func isEligible(forProvider providerName: ProviderName) -> Bool {
@ -221,16 +211,34 @@ extension ProductManager {
public func isEligibleForFeedback() -> Bool { public func isEligibleForFeedback() -> Bool {
appType == .beta || !purchasedFeatures.isEmpty appType == .beta || !purchasedFeatures.isEmpty
} }
public func hasPurchased(_ product: LocalProduct) -> Bool {
purchasedFeatures.contains(product)
} }
public func purchaseDate(forProduct product: LocalProduct) -> Date? { extension ProductManager {
purchaseDates[product] func isActivePurchase(_ feature: LocalProduct) -> Bool {
purchasedFeatures.contains(feature) && cancelledPurchases?.contains(feature) == false
}
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 { extension ProductManager {
public func reloadReceipt(andNotify: Bool = true) { public func reloadReceipt(andNotify: Bool = true) {
guard let receipt = receiptReader.receipt(for: appType) else { guard let receipt = receiptReader.receipt(for: appType) else {
@ -280,6 +288,8 @@ extension ProductManager {
} }
} }
// MARK: Helpers
private extension ProductManager { private extension ProductManager {
var isMac: Bool { var isMac: Bool {
#if targetEnvironment(macCatalyst) #if targetEnvironment(macCatalyst)

View File

@ -120,6 +120,7 @@ final class ProductManagerTests: XCTestCase {
reader.setReceipt(withBuild: 1500, products: [.fullVersion]) reader.setReceipt(withBuild: 1500, products: [.fullVersion])
let sut = ProductManager(inApp: inApp, receiptReader: reader, buildProducts: noBuildProducts) let sut = ProductManager(inApp: inApp, receiptReader: reader, buildProducts: noBuildProducts)
XCTAssertTrue(sut.isFullVersion())
XCTAssertTrue(LocalProduct XCTAssertTrue(LocalProduct
.allFeatures .allFeatures
.filter { !$0.isPlatformVersion } .filter { !$0.isPlatformVersion }
@ -132,6 +133,7 @@ final class ProductManagerTests: XCTestCase {
reader.setReceipt(withBuild: 1500, products: []) reader.setReceipt(withBuild: 1500, products: [])
let sut = ProductManager(inApp: inApp, receiptReader: reader, buildProducts: noBuildProducts) let sut = ProductManager(inApp: inApp, receiptReader: reader, buildProducts: noBuildProducts)
XCTAssertFalse(sut.isFullVersion())
XCTAssertFalse(LocalProduct XCTAssertFalse(LocalProduct
.allFeatures .allFeatures
.allSatisfy(sut.isEligible(forFeature:)) .allSatisfy(sut.isEligible(forFeature:))