From e21e11b022db1194202248ed9fc73a9eee583dc0 Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Thu, 11 Jan 2024 16:46:52 +0100 Subject: [PATCH] Rework how purchasable products are presented (#465) Fixes #464 --- CHANGELOG.md | 6 ++ .../App/Views/PaywallView+Purchase.swift | 28 ++------ .../Managers/ProductManager.swift | 38 ++++++++++ .../ProductManagerTests.swift | 70 +++++++++++++++++++ 4 files changed, 118 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6181c25e..2e404996 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Fixed + +- Platform purchasers cannot upgrade to full version. [#464](https://github.com/passepartoutvpn/passepartout-apple/issues/464) + ## 2.3.2 (2024-01-11) ### Fixed diff --git a/Passepartout/App/Views/PaywallView+Purchase.swift b/Passepartout/App/Views/PaywallView+Purchase.swift index 49287a18..9593e19c 100644 --- a/Passepartout/App/Views/PaywallView+Purchase.swift +++ b/Passepartout/App/Views/PaywallView+Purchase.swift @@ -194,29 +194,8 @@ private extension PaywallView.PurchaseView { } private extension PaywallView.PurchaseView { - - // hide full version if already bought the other platform version var skFullVersion: InAppProduct? { - guard !productManager.isFullVersion() else { - return nil - } - #if targetEnvironment(macCatalyst) - guard !productManager.hasPurchased(.fullVersion_iOS) else { - return nil - } - #else - guard !productManager.hasPurchased(.fullVersion_macOS) else { - return nil - } - #endif - return productManager.product(withIdentifier: .fullVersion) - } - - var skAppleTV: InAppProduct? { - guard feature == .appleTV else { - return nil - } - return productManager.product(withIdentifier: .appleTV) + productManager.product(withIdentifier: .fullVersion) } var fullFeatures: [FeatureModel] { @@ -230,8 +209,9 @@ private extension PaywallView.PurchaseView { } var productRowModels: [InAppProduct] { - [skFullVersion, skAppleTV] - .compactMap { $0 } + productManager + .purchasableProducts(withFeature: feature) + .compactMap { productManager.product(withIdentifier: $0) } } } diff --git a/PassepartoutLibrary/Sources/PassepartoutFrontend/Managers/ProductManager.swift b/PassepartoutLibrary/Sources/PassepartoutFrontend/Managers/ProductManager.swift index 332d3281..c660730c 100644 --- a/PassepartoutLibrary/Sources/PassepartoutFrontend/Managers/ProductManager.swift +++ b/PassepartoutLibrary/Sources/PassepartoutFrontend/Managers/ProductManager.swift @@ -293,6 +293,44 @@ extension ProductManager { } } +// MARK: Purchasable products + +extension ProductManager { + + // no purchase -> full version or platform version + // purchased platform -> may only purchase other platform + + public func purchasableProducts(withFeature feature: LocalProduct?) -> [LocalProduct] { + var products: [LocalProduct] = { + if hasPurchased(.fullVersion) { + return [] + } +#if targetEnvironment(macCatalyst) + if hasPurchased(.fullVersion_macOS) { + return [] + } + if hasPurchased(.fullVersion_iOS) { + return [.fullVersion_macOS] + } + return [.fullVersion, .fullVersion_macOS] +#else + if hasPurchased(.fullVersion_iOS) { + return [] + } + if hasPurchased(.fullVersion_macOS) { + return [.fullVersion_iOS] + } + return [.fullVersion, .fullVersion_iOS] +#endif + }() + if feature == .appleTV && !hasPurchased(.appleTV) { + products.append(.appleTV) + } + return products + } + +} + // MARK: Helpers private extension ProductManager { diff --git a/PassepartoutLibrary/Tests/PassepartoutFrontendTests/ProductManagerTests.swift b/PassepartoutLibrary/Tests/PassepartoutFrontendTests/ProductManagerTests.swift index 4bd02410..ecd5413e 100644 --- a/PassepartoutLibrary/Tests/PassepartoutFrontendTests/ProductManagerTests.swift +++ b/PassepartoutLibrary/Tests/PassepartoutFrontendTests/ProductManagerTests.swift @@ -33,6 +33,8 @@ final class ProductManagerTests: XCTestCase { private let noBuildProducts = BuildProducts { _ in [] } + // MARK: Build products + func test_givenBuildProducts_whenOlder_thenFullVersion() { let reader = MockReceiptReader() reader.setReceipt(withBuild: 500, products: []) @@ -57,6 +59,8 @@ final class ProductManagerTests: XCTestCase { XCTAssertFalse(sut.isEligible(forFeature: .fullVersion)) } + // MARK: Eligibility + func test_givenPurchase_whenReload_thenCredited() { let reader = MockReceiptReader() let sut = ProductManager(inApp: inApp, receiptReader: reader, buildProducts: noBuildProducts) @@ -163,4 +167,70 @@ final class ProductManagerTests: XCTestCase { XCTAssertTrue(sut.isEligible(forFeature: .appleTV)) } + + // MARK: Purchasable + + func test_givenNoPurchase_thenCanBuyFullAndPlatformVersion() { + let reader = MockReceiptReader() + reader.setReceipt(withBuild: 1500, products: []) + let sut = ProductManager(inApp: inApp, receiptReader: reader, buildProducts: noBuildProducts) + +#if targetEnvironment(macCatalyst) + XCTAssertEqual(sut.purchasableProducts(withFeature: nil), [.fullVersion, .fullVersion_macOS]) +#else + XCTAssertEqual(sut.purchasableProducts(withFeature: nil), [.fullVersion, .fullVersion_iOS]) +#endif + } + + func test_givenFullVersion_thenCannotPurchase() { + let reader = MockReceiptReader() + + reader.setReceipt(withBuild: 1500, products: [.fullVersion]) + let sut = ProductManager(inApp: inApp, receiptReader: reader, buildProducts: noBuildProducts) + XCTAssertEqual(sut.purchasableProducts(withFeature: nil), []) + } + + func test_givenPlatformVersion_thenCannotPurchaseSamePlatform() { + let reader = MockReceiptReader() + +#if targetEnvironment(macCatalyst) + reader.setReceipt(withBuild: 1500, products: [.fullVersion_macOS]) + let sut = ProductManager(inApp: inApp, receiptReader: reader, buildProducts: noBuildProducts) + XCTAssertEqual(sut.purchasableProducts(withFeature: nil), []) +#else + reader.setReceipt(withBuild: 1500, products: [.fullVersion_iOS]) + let sut = ProductManager(inApp: inApp, receiptReader: reader, buildProducts: noBuildProducts) + XCTAssertEqual(sut.purchasableProducts(withFeature: nil), []) +#endif + } + + func test_givenOtherPlatformVersion_thenCanOnlyPurchaseMissingPlatform() { + let reader = MockReceiptReader() + +#if targetEnvironment(macCatalyst) + reader.setReceipt(withBuild: 1500, products: [.fullVersion_iOS]) + let sut = ProductManager(inApp: inApp, receiptReader: reader, buildProducts: noBuildProducts) + XCTAssertEqual(sut.purchasableProducts(withFeature: nil), [.fullVersion_macOS]) +#else + reader.setReceipt(withBuild: 1500, products: [.fullVersion_macOS]) + let sut = ProductManager(inApp: inApp, receiptReader: reader, buildProducts: noBuildProducts) + XCTAssertEqual(sut.purchasableProducts(withFeature: nil), [.fullVersion_iOS]) +#endif + } + + func test_givenAppleTV_whenDidNotPurchase_thenCanPurchase() { + let reader = MockReceiptReader() + reader.setReceipt(withBuild: 1500, products: []) + let sut = ProductManager(inApp: inApp, receiptReader: reader, buildProducts: noBuildProducts) + + XCTAssertTrue(sut.purchasableProducts(withFeature: .appleTV).contains(.appleTV)) + } + + func test_givenAppleTV_whenDidPurchase_thenCannotPurchase() { + let reader = MockReceiptReader() + reader.setReceipt(withBuild: 1500, products: [.appleTV]) + let sut = ProductManager(inApp: inApp, receiptReader: reader, buildProducts: noBuildProducts) + + XCTAssertFalse(sut.purchasableProducts(withFeature: .appleTV).contains(.appleTV)) + } }