Simplify products (#1024)

Only offer compensations to former purchasers:

- .appleTV to .full purchasers
- .full to .appleTV purchasers

Always suggest .fullTV to new purchasers.

Finally rename:

- .full to .iOS_macOS
- .fullTV to .allFeatures (lifetime)
This commit is contained in:
Davide 2024-12-18 10:15:58 +01:00 committed by GitHub
parent 2eca757dc6
commit c527171957
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 91 additions and 130 deletions

View File

@ -37,11 +37,11 @@ extension AppProduct {
public enum Full {
static let all: [AppProduct] = [
.Full.OneTime.full,
.Full.OneTime.fullTV,
.Full.OneTime.allFeatures,
.Full.Recurring.monthly,
.Full.Recurring.yearly,
//
.Full.OneTime.iOS_macOS,
.Full.OneTime.iOS,
.Full.OneTime.macOS
]
@ -66,12 +66,7 @@ extension AppProduct.Features {
extension AppProduct.Full {
public enum OneTime {
// iOS/macOS
public static let full = AppProduct(featureId: "full_multi_version")
// iOS/macOS + tvOS
public static let fullTV = AppProduct(featureId: "full.lifetime")
public static let allFeatures = AppProduct(featureId: "full.lifetime")
}
public enum Recurring {
@ -84,7 +79,10 @@ extension AppProduct.Full {
extension AppProduct {
public var isFullVersion: Bool {
switch self {
case .Full.OneTime.full, .Full.OneTime.fullTV, .Full.Recurring.monthly, .Full.Recurring.yearly:
case .Full.Recurring.yearly,
.Full.Recurring.monthly,
.Full.OneTime.allFeatures,
.Full.OneTime.iOS_macOS:
return true
default:
return false
@ -113,6 +111,9 @@ extension AppProduct.Features {
extension AppProduct.Full.OneTime {
@available(*, deprecated)
public static let iOS_macOS = AppProduct(featureId: "full_multi_version")
@available(*, deprecated)
public static let iOS = AppProduct(featureId: "full_version")

View File

@ -32,9 +32,9 @@ public enum AppUserLevel: Int, Sendable {
case beta = 1
case full = 2
case fullV2 = 2 // without .appleTV
case fullTV = 3
case fullV3 = 3
}
extension AppUserLevel {

View File

@ -1,34 +0,0 @@
//
// AppFeature+Full.swift
// Passepartout
//
// Created by Davide De Rosa on 11/20/24.
// Copyright (c) 2024 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
extension AppFeature {
public static let fullFeatures = AppFeature.allCases.filter {
$0 != .appleTV
}
public static let fullTVFeatures = AppFeature.allCases
}

View File

@ -35,11 +35,11 @@ extension AppUserLevel: AppFeatureProviding {
.sharing
]
case .full:
return AppFeature.fullFeatures
case .fullV2:
return AppProduct.Full.OneTime.iOS_macOS.features
case .fullTV:
return AppFeature.fullTVFeatures
case .fullV3:
return AppProduct.Full.OneTime.allFeatures.features
default:
return []

View File

@ -31,27 +31,29 @@ extension AppProduct: AppFeatureProviding {
// MARK: Current
case .Full.OneTime.full:
return AppFeature.fullFeatures
case .Full.OneTime.fullTV, .Full.Recurring.monthly, .Full.Recurring.yearly:
return AppFeature.fullTVFeatures
case .Full.OneTime.allFeatures, .Full.Recurring.monthly, .Full.Recurring.yearly:
return AppFeature.allCases
case .Features.appleTV:
return [.appleTV, .sharing]
// MARK: Discontinued
case .Full.OneTime.iOS_macOS:
return AppFeature.allCases.filter {
$0 != .appleTV
}
case .Full.OneTime.iOS:
#if os(iOS)
return AppFeature.fullFeatures
return AppProduct.Full.OneTime.iOS_macOS.features
#else
return []
#endif
case .Full.OneTime.macOS:
#if os(macOS)
return AppFeature.fullFeatures
return AppProduct.Full.OneTime.iOS_macOS.features
#else
return []
#endif

View File

@ -30,7 +30,7 @@ import PassepartoutKit
extension IAPManager {
public var isFullVersionPurchaser: Bool {
purchasedProducts.contains(.Full.OneTime.full) || purchasedProducts.contains(.Full.OneTime.fullTV) || (purchasedProducts.contains(.Full.OneTime.iOS) && purchasedProducts.contains(.Full.OneTime.macOS))
purchasedProducts.contains(where: \.isFullVersion) || (purchasedProducts.contains(.Full.OneTime.iOS) && purchasedProducts.contains(.Full.OneTime.macOS))
}
public func suggestedProducts(for requiredFeatures: Set<AppFeature>, withRecurring: Bool = true) -> Set<AppProduct>? {
@ -51,20 +51,14 @@ extension IAPManager {
assertionFailure("Full version purchaser requiring other than [.appleTV]")
}
} else { // !isFullVersionPurchaser
if ineligibleFeatures == [.appleTV] {
products.insert(.Features.appleTV)
products.insert(.Full.OneTime.fullTV)
} else if ineligibleFeatures.contains(.appleTV) {
products.insert(.Full.OneTime.fullTV)
if eligibleFeatures.contains(.appleTV) {
products.insert(.Full.OneTime.iOS_macOS)
} else {
if !eligibleFeatures.contains(.appleTV) {
products.insert(.Full.OneTime.fullTV)
}
products.insert(.Full.OneTime.full)
products.insert(.Full.OneTime.allFeatures)
}
}
if withRecurring && products.contains(.Full.OneTime.fullTV) {
if withRecurring && products.contains(.Full.OneTime.allFeatures) {
products.insert(.Full.Recurring.monthly)
products.insert(.Full.Recurring.yearly)
}

View File

@ -31,7 +31,7 @@ import PassepartoutKit
extension AppContext {
public static let forPreviews: AppContext = {
let iapManager = IAPManager(
customUserLevel: .fullTV,
customUserLevel: .fullV3,
inAppHelper: FakeAppProductHelper(),
receiptReader: FakeAppReceiptReader(),
betaChecker: TestFlightChecker(),

View File

@ -90,7 +90,7 @@ private extension PaywallView {
featureProductsView
fullProductsView
if !iapManager.isFullVersionPurchaser {
fullVersionFeaturesView
allFeaturesView
}
restoreView
}
@ -142,11 +142,11 @@ private extension PaywallView {
}
}
var fullVersionFeaturesView: some View {
var allFeaturesView: some View {
FeatureListView(
style: allFeaturesStyle,
header: Strings.Views.Paywall.Sections.AllFeatures.header,
features: fullVersionFeatures,
features: allFeatures,
content: featureView(for:)
)
}
@ -197,8 +197,8 @@ private extension PaywallView {
// MARK: -
private extension PaywallView {
var fullVersionFeatures: [AppFeature] {
AppFeature.fullFeatures
var allFeatures: [AppFeature] {
AppProduct.Full.OneTime.allFeatures.features
}
func fetchAvailableProducts() async {
@ -285,9 +285,9 @@ private extension AppProduct {
return .min
case .Full.Recurring.monthly:
return 1
case .Full.OneTime.fullTV:
case .Full.OneTime.allFeatures:
return 2
case .Full.OneTime.full:
case .Full.OneTime.iOS_macOS:
return 3
default:
return .max

View File

@ -49,7 +49,7 @@ extension IAPManagerTests {
let sut = IAPManager(receiptReader: reader)
let appProducts: [AppProduct] = [
.Full.OneTime.full,
.Full.OneTime.iOS_macOS,
.Donations.huge
]
let inAppProducts = try await sut.purchasableProducts(for: appProducts)
@ -88,12 +88,12 @@ extension IAPManagerTests {
await reader.setReceipt(withBuild: olderBuildNumber, identifiers: [])
let sut = IAPManager(receiptReader: reader) { build in
if build <= self.defaultBuildNumber {
return [.Full.OneTime.full]
return [.Full.OneTime.iOS_macOS]
}
return []
}
await sut.reloadReceipt()
XCTAssertTrue(sut.isEligible(for: AppFeature.fullFeatures))
XCTAssertTrue(sut.isEligible(for: AppFeature.fullV2Features))
}
func test_givenBuildProducts_whenNewer_thenFreeVersion() async {
@ -101,12 +101,12 @@ extension IAPManagerTests {
await reader.setReceipt(withBuild: newerBuildNumber, products: [])
let sut = IAPManager(receiptReader: reader) { build in
if build <= self.defaultBuildNumber {
return [.Full.OneTime.full]
return [.Full.OneTime.iOS_macOS]
}
return []
}
await sut.reloadReceipt()
XCTAssertFalse(sut.isEligible(for: AppFeature.fullFeatures))
XCTAssertFalse(sut.isEligible(for: AppFeature.fullV2Features))
}
}
@ -117,13 +117,13 @@ extension IAPManagerTests {
let reader = FakeAppReceiptReader()
let sut = IAPManager(receiptReader: reader)
XCTAssertFalse(sut.isEligible(for: AppFeature.fullFeatures))
XCTAssertFalse(sut.isEligible(for: AppFeature.fullV2Features))
await reader.setReceipt(withBuild: defaultBuildNumber, products: [.Full.OneTime.full])
XCTAssertFalse(sut.isEligible(for: AppFeature.fullFeatures))
await reader.setReceipt(withBuild: defaultBuildNumber, products: [.Full.OneTime.iOS_macOS])
XCTAssertFalse(sut.isEligible(for: AppFeature.fullV2Features))
await sut.reloadReceipt()
XCTAssertTrue(sut.isEligible(for: AppFeature.fullFeatures))
XCTAssertTrue(sut.isEligible(for: AppFeature.fullV2Features))
}
func test_givenPurchasedFeatures_thenIsOnlyEligibleForFeatures() async {
@ -139,20 +139,20 @@ extension IAPManagerTests {
XCTAssertFalse(sut.isEligible(for: .onDemand))
XCTAssertTrue(sut.isEligible(for: .routing))
XCTAssertFalse(sut.isEligible(for: .sharing))
XCTAssertFalse(sut.isEligible(for: AppFeature.fullFeatures))
XCTAssertFalse(sut.isEligible(for: AppFeature.fullV2Features))
}
func test_givenPurchasedAndCancelledFeature_thenIsNotEligible() async {
let reader = FakeAppReceiptReader()
await reader.setReceipt(
withBuild: defaultBuildNumber,
products: [.Full.OneTime.full],
cancelledProducts: [.Full.OneTime.full]
products: [.Full.OneTime.iOS_macOS],
cancelledProducts: [.Full.OneTime.iOS_macOS]
)
let sut = IAPManager(receiptReader: reader)
await sut.reloadReceipt()
XCTAssertFalse(sut.isEligible(for: AppFeature.fullFeatures))
XCTAssertFalse(sut.isEligible(for: AppFeature.fullV2Features))
}
func test_givenFreeVersion_thenIsNotEligibleForAnyFeature() async {
@ -161,7 +161,7 @@ extension IAPManagerTests {
let sut = IAPManager(receiptReader: reader)
await sut.reloadReceipt()
AppFeature.fullFeatures.forEach {
AppFeature.fullV2Features.forEach {
XCTAssertFalse(sut.isEligible(for: $0))
}
}
@ -177,7 +177,7 @@ extension IAPManagerTests {
func test_givenFullV2Version_thenIsEligibleForAnyFeatureExceptExcluded() async {
let reader = FakeAppReceiptReader()
await reader.setReceipt(withBuild: defaultBuildNumber, products: [.Full.OneTime.full])
await reader.setReceipt(withBuild: defaultBuildNumber, products: [.Full.OneTime.iOS_macOS])
let sut = IAPManager(receiptReader: reader)
await sut.reloadReceipt()
@ -186,7 +186,7 @@ extension IAPManagerTests {
.interactiveLogin
]
AppFeature.allCases.forEach {
if AppFeature.fullFeatures.contains($0) {
if AppFeature.fullV2Features.contains($0) {
XCTAssertTrue(sut.isEligible(for: $0))
} else {
XCTAssertTrue(excluded.contains($0))
@ -212,7 +212,7 @@ extension IAPManagerTests {
#if os(macOS)
await reader.setReceipt(withBuild: defaultBuildNumber, products: [.Full.OneTime.macOS, .Features.networkSettings])
await sut.reloadReceipt()
XCTAssertTrue(sut.isEligible(for: AppFeature.fullFeatures))
XCTAssertTrue(sut.isEligible(for: AppFeature.fullV2Features))
#else
await reader.setReceipt(withBuild: defaultBuildNumber, products: [.Full.OneTime.iOS, .Features.networkSettings])
await sut.reloadReceipt()
@ -227,7 +227,7 @@ extension IAPManagerTests {
#if os(macOS)
await reader.setReceipt(withBuild: defaultBuildNumber, products: [.Full.OneTime.iOS, .Features.networkSettings])
await sut.reloadReceipt()
XCTAssertFalse(sut.isEligible(for: AppFeature.fullFeatures))
XCTAssertFalse(sut.isEligible(for: AppFeature.fullV2Features))
#else
await reader.setReceipt(withBuild: defaultBuildNumber, products: [.Full.OneTime.macOS, .Features.networkSettings])
await sut.reloadReceipt()
@ -266,23 +266,21 @@ extension IAPManagerTests {
XCTAssertNil(sut.suggestedProducts(for: []))
}
func test_givenFree_whenRequireFeature_thenSuggestsFullAndFullTV() async {
func test_givenFree_whenRequireFeature_thenSuggestsFullTV() async {
let sut = await IAPManager(products: [])
XCTAssertEqual(sut.suggestedProducts(for: [.dns]), [
.Full.Recurring.yearly,
.Full.Recurring.monthly,
.Full.OneTime.full,
.Full.OneTime.fullTV
.Full.OneTime.allFeatures
])
}
func test_givenFree_whenRequireAppleTV_thenSuggestsAppleTVAndFullTV() async {
func test_givenFree_whenRequireAppleTV_thenSuggestsFullTV() async {
let sut = await IAPManager(products: [])
XCTAssertEqual(sut.suggestedProducts(for: [.appleTV]), [
.Features.appleTV,
.Full.Recurring.yearly,
.Full.Recurring.monthly,
.Full.OneTime.fullTV
.Full.OneTime.allFeatures
])
}
@ -291,7 +289,7 @@ extension IAPManagerTests {
XCTAssertEqual(sut.suggestedProducts(for: [.appleTV, .providers]), [
.Full.Recurring.yearly,
.Full.Recurring.monthly,
.Full.OneTime.fullTV
.Full.OneTime.allFeatures
])
}
@ -300,43 +298,39 @@ extension IAPManagerTests {
XCTAssertNil(sut.suggestedProducts(for: [.dns]))
}
func test_givenCurrentPlatform_whenRequireAppleTV_thenSuggestsAppleTVAndFullTV() async {
func test_givenCurrentPlatform_whenRequireAppleTV_thenSuggestsFullTV() async {
let sut = await IAPManager.withFullCurrentPlatform()
XCTAssertEqual(sut.suggestedProducts(for: [.appleTV]), [
.Features.appleTV,
.Full.Recurring.yearly,
.Full.Recurring.monthly,
.Full.OneTime.fullTV
.Full.OneTime.allFeatures
])
}
func test_givenCurrentPlatform_whenRequireFeatureAndAppleTV_thenSuggestsAppleTVAndFullTV() async {
func test_givenCurrentPlatform_whenRequireFeatureAndAppleTV_thenSuggestsFullTV() async {
let sut = await IAPManager.withFullCurrentPlatform()
XCTAssertEqual(sut.suggestedProducts(for: [.appleTV, .providers]), [
.Features.appleTV,
.Full.Recurring.yearly,
.Full.Recurring.monthly,
.Full.OneTime.fullTV
.Full.OneTime.allFeatures
])
}
func test_givenOtherPlatform_whenRequireFeature_thenSuggestsFullAndFullTV() async {
func test_givenOtherPlatform_whenRequireFeature_thenSuggestsFullTV() async {
let sut = await IAPManager.withFullOtherPlatform()
XCTAssertEqual(sut.suggestedProducts(for: [.dns]), [
.Full.Recurring.yearly,
.Full.Recurring.monthly,
.Full.OneTime.fullTV,
.Full.OneTime.full
.Full.OneTime.allFeatures
])
}
func test_givenOtherPlatform_whenRequireAppleTV_thenSuggestsAppleTVAndFullTV() async {
func test_givenOtherPlatform_whenRequireAppleTV_thenSuggestsFullTV() async {
let sut = await IAPManager.withFullOtherPlatform()
XCTAssertEqual(sut.suggestedProducts(for: [.appleTV]), [
.Features.appleTV,
.Full.Recurring.yearly,
.Full.Recurring.monthly,
.Full.OneTime.fullTV
.Full.OneTime.allFeatures
])
}
@ -345,24 +339,24 @@ extension IAPManagerTests {
XCTAssertEqual(sut.suggestedProducts(for: [.appleTV, .providers]), [
.Full.Recurring.yearly,
.Full.Recurring.monthly,
.Full.OneTime.fullTV
.Full.OneTime.allFeatures
])
}
func test_givenFull_whenRequireFeature_thenSuggestsNothing() async {
let sut = await IAPManager(products: [.Full.OneTime.full])
let sut = await IAPManager(products: [.Full.OneTime.iOS_macOS])
XCTAssertNil(sut.suggestedProducts(for: [.dns]))
}
func test_givenFull_whenRequireAppleTV_thenSuggestsAppleTV() async {
let sut = await IAPManager(products: [.Full.OneTime.full])
let sut = await IAPManager(products: [.Full.OneTime.iOS_macOS])
XCTAssertEqual(sut.suggestedProducts(for: [.appleTV]), [
.Features.appleTV
])
}
func test_givenFull_whenRequireFeatureAndAppleTV_thenSuggestsAppleTV() async {
let sut = await IAPManager(products: [.Full.OneTime.full])
let sut = await IAPManager(products: [.Full.OneTime.iOS_macOS])
XCTAssertEqual(sut.suggestedProducts(for: [.appleTV, .providers]), [
.Features.appleTV
])
@ -371,7 +365,7 @@ extension IAPManagerTests {
func test_givenAppleTV_whenRequireFeature_thenSuggestsFull() async {
let sut = await IAPManager(products: [.Features.appleTV])
XCTAssertEqual(sut.suggestedProducts(for: [.dns]), [
.Full.OneTime.full
.Full.OneTime.iOS_macOS
])
}
@ -383,22 +377,22 @@ extension IAPManagerTests {
func test_givenAppleTV_whenRequireFeatureAndAppleTV_thenSuggestsFull() async {
let sut = await IAPManager(products: [.Features.appleTV])
XCTAssertEqual(sut.suggestedProducts(for: [.appleTV, .providers]), [
.Full.OneTime.full
.Full.OneTime.iOS_macOS
])
}
func test_givenAll_whenRequireFeature_thenSuggestsNothing() async {
let sut = await IAPManager(products: [.Full.OneTime.fullTV])
func test_givenFullTV_whenRequireFeature_thenSuggestsNothing() async {
let sut = await IAPManager(products: [.Full.OneTime.allFeatures])
XCTAssertNil(sut.suggestedProducts(for: [.dns]))
}
func test_givenAll_whenRequireAppleTV_thenSuggestsNothing() async {
let sut = await IAPManager(products: [.Full.OneTime.fullTV])
func test_givenFullTV_whenRequireAppleTV_thenSuggestsNothing() async {
let sut = await IAPManager(products: [.Full.OneTime.allFeatures])
XCTAssertNil(sut.suggestedProducts(for: [.appleTV]))
}
func test_givenAll_whenRequireFeatureAndAppleTV_thenSuggestsNothing() async {
let sut = await IAPManager(products: [.Full.OneTime.fullTV])
func test_givenFullTV_whenRequireFeatureAndAppleTV_thenSuggestsNothing() async {
let sut = await IAPManager(products: [.Full.OneTime.allFeatures])
XCTAssertNil(sut.suggestedProducts(for: [.appleTV, .providers]))
}
}
@ -446,7 +440,7 @@ extension IAPManagerTests {
func test_givenFullV2App_thenIsEligibleForAnyFeatureExceptExcluded() async {
let reader = FakeAppReceiptReader()
let sut = IAPManager(customUserLevel: .full, receiptReader: reader)
let sut = IAPManager(customUserLevel: .fullV2, receiptReader: reader)
await sut.reloadReceipt()
let excluded: Set<AppFeature> = [
@ -454,7 +448,7 @@ extension IAPManagerTests {
.interactiveLogin
]
AppFeature.allCases.forEach {
if AppFeature.fullFeatures.contains($0) {
if AppFeature.fullV2Features.contains($0) {
XCTAssertTrue(sut.isEligible(for: $0))
} else {
XCTAssertTrue(excluded.contains($0))
@ -465,10 +459,10 @@ extension IAPManagerTests {
func test_givenSubscriberApp_thenIsEligibleForAnyFeature() async {
let reader = FakeAppReceiptReader()
let sut = IAPManager(customUserLevel: .fullTV, receiptReader: reader)
let sut = IAPManager(customUserLevel: .fullV3, receiptReader: reader)
await sut.reloadReceipt()
AppFeature.fullFeatures.forEach {
AppFeature.fullV2Features.forEach {
XCTAssertTrue(sut.isEligible(for: $0))
}
XCTAssertTrue(sut.isEligible(for: .appleTV))
@ -545,7 +539,7 @@ extension IAPManagerTests {
extension IAPManagerTests {
func test_givenManager_whenObserveObjects_thenReloadsReceipt() async {
let reader = FakeAppReceiptReader()
await reader.setReceipt(withBuild: .max, products: [.Full.OneTime.full])
await reader.setReceipt(withBuild: .max, products: [.Full.OneTime.iOS_macOS])
let sut = IAPManager(receiptReader: reader)
XCTAssertEqual(sut.userLevel, .undefined)
@ -620,3 +614,7 @@ private extension IAPManager {
#endif
}
}
private extension AppFeature {
static let fullV2Features = AppProduct.Full.OneTime.iOS_macOS.features
}

View File

@ -33,7 +33,7 @@ extension AppContext {
static func forUITesting(withRegistry registry: Registry) -> AppContext {
let dependencies: Dependencies = .shared
let iapManager = IAPManager(
customUserLevel: .fullTV,
customUserLevel: .fullV3,
inAppHelper: dependencies.appProductHelper(),
receiptReader: FakeAppReceiptReader(),
betaChecker: TestFlightChecker(),