-
Notifications
You must be signed in to change notification settings - Fork 21
Expand file tree
/
Copy pathPurchaseRequirement.swift
More file actions
190 lines (169 loc) · 8.6 KB
/
PurchaseRequirement.swift
File metadata and controls
190 lines (169 loc) · 8.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
//
// PurchaseRequirement.swift
// FlintCore
//
// Created by Marc Palmer on 03/04/2018.
// Copyright © 2018 Montana Floss Co. Ltd. All rights reserved.
//
import Foundation
/// Use a `PurchaseRequirement` to express the rules about what purchased products enable your Feature(s).
///
/// You can express complex rules about how your Features are enabled using a graph of requirements.
/// Each Feature can have multiple purchase requirements (combined with AND),
/// but one requirement can match one or all of a list of product IDs, as well as having dependencies on other requirements.
///
/// With this you can express the following kinds of rules:
///
/// * Feature X is available if Product A is purchased
/// * Feature X is available if Product A OR Product B OR Product C is purchased
/// * Feature X is available if Product A AND Product B AND Product C is purchased
/// * Feature X is available if Product A AND (Product B OR Product C) is purchased
/// * Feature X is available if (Product A OR Product B) AND ((Product B OR Product C) AND PRODUCT D) is purchased
/// * Feature X is available if (Product A OR Product B) AND ((Product B OR Product C) AND PRODUCT D AND PRODUCT E) is purchased
///
/// ...and so on. This allows you to map Feature availability to a range of different product pricing strategies and relationships,
/// such as "Basic" level of subscription plus a "Founder" IAP that maybe offered to unlock all features in future for a one-off purchase,
/// provided they still have a basic subscription.
public class PurchaseRequirement: Hashable, Equatable, CustomStringConvertible {
/// An enum type that determines how a products are matched.
public enum Criteria: Hashable, Equatable {
/// The requirement rule will be met if any of the products have been purchased
case any
/// The requirement rule will be met only if all of the products have been purchased
case all
}
/// The set of products to match with this requirement
public let products: Set<Product>
/// Determines how products are matched. Specifying `.any` means at least one product has to be purchased to fulfull this requirement.
/// Using `.all` means every product in the `products` set must be purchased for this requirement to be fulfilled.
public let matchingCriteria: Criteria
/// Optional list of requirements that must also be fulfilled for this requirement to be fulfilled.
/// Using this you can express complex combinations of purchases and options
public let dependencies: [PurchaseRequirement]?
/// The optional quantity of product for this requirement to be met.
/// This is optional and only relates to ConsumableProduct types, and is *purely informational* because
/// Flint does not handle allocation of consumable products/credits, but your app can access this information
/// when you need to show a store UI to unlock the feature.
public let quantity: UInt?
/// Initialise the requirement with its products, matching criteria and dependencies.
init(products: Set<Product>, quantity: UInt?, matchingCriteria: Criteria, dependencies: [PurchaseRequirement]? = nil) {
self.products = products
self.quantity = quantity
self.matchingCriteria = matchingCriteria
self.dependencies = dependencies
}
/// Initialise the requirement with its products, matching criteria and dependencies.
public convenience init(products: Set<NonConsumableProduct>, matchingCriteria: Criteria, dependencies: [PurchaseRequirement]? = nil) {
self.init(products: products, quantity: nil, matchingCriteria: matchingCriteria, dependencies: dependencies)
}
public convenience init(_ product: NonConsumableProduct, dependencies: [PurchaseRequirement]? = nil) {
self.init(products: [product], matchingCriteria: .all, dependencies: dependencies)
}
public convenience init(_ product: SubscriptionProduct, dependencies: [PurchaseRequirement]? = nil) {
self.init(products: [product], quantity: nil, matchingCriteria: .all, dependencies: dependencies)
}
public convenience init(_ product: ConsumableProduct, quantity: UInt, dependencies: [PurchaseRequirement]? = nil) {
self.init(products: [product], quantity: quantity, matchingCriteria: .all, dependencies: dependencies)
}
/// Call to see if this requirement and all dependent requirements are fulfilled
/// - param validator: The validator to use to see if each product in a requirement has been purchased
public func isFulfilled(purchaseTracker: PurchaseTracker, feature: ConditionalFeatureDefinition.Type) -> Bool? {
let matched: Bool?
switch matchingCriteria {
case .any:
var result: Bool?
for product in products {
result = establishFulfilment(of: product, purchaseTracker: purchaseTracker, feature: feature)
if result == true {
break
}
}
matched = result
case .all:
var result: Bool?
for product in products {
result = establishFulfilment(of: product, purchaseTracker: purchaseTracker, feature: feature)
if !(result == true) {
break
}
}
matched = result
}
// Only evaluate dependencies if this level's requirements are met, or there are no direct requirements at this level
if matched == true || products.count == 0 {
guard let dependencies = dependencies else {
return matched
}
let firstFailing = dependencies.first(where: { requirement -> Bool in
return !(requirement.isFulfilled(purchaseTracker: purchaseTracker, feature: feature) == true)
})
return firstFailing == nil
} else {
return matched
}
}
private func establishFulfilment(of product: Product,
purchaseTracker: PurchaseTracker,
feature: ConditionalFeatureDefinition.Type) -> Bool? {
var purchased: Bool?
switch product {
case let nonConsumableProduct as NonConsumableProduct:
purchased = purchaseTracker.isPurchased(nonConsumableProduct)
case let subscriptionProduct as SubscriptionProduct:
purchased = purchaseTracker.isSubscriptionActive(subscriptionProduct)
case is ConsumableProduct:
break
default:
flintBug("Unsupported product type: \(product)")
}
if purchased == nil || purchased == false {
if purchaseTracker.isFeatureEnabledByPastPurchases(feature) {
purchased = true
}
}
return purchased
}
public var description: String {
let productDescriptions: [String] = products.map {
if let descriptionText = $0.description {
return "\($0.productID): \"\(descriptionText)\""
} else {
return $0.productID
}
}
let text = "Purchase requirement for \(productDescriptions.joined(separator: ", ")) (matching: \(matchingCriteria))"
if let dependencies = dependencies, dependencies.count > 0 {
let dependencyDescriptions = dependencies.map { $0.description }
return text + " dependencies: \(dependencyDescriptions.joined(separator: ", "))"
} else {
return text
}
}
/// Internal API to enumerate all products referenced by this requirement
func allReferencedProducts() -> Set<Product> {
var results = Set<Product>()
results.formUnion(products)
// !!! TODO: prevent dependency cycles
if let dependencies = dependencies {
for dependency in dependencies {
results.formUnion(dependency.allReferencedProducts())
}
}
return results
}
// MARK: Hashable & Equatable Conformances
#if swift(>=4.2)
public func hash(into hasher: inout Hasher) {
hasher.combine(products.hashValue ^ matchingCriteria.hashValue)
}
#else
public var hashValue: Int {
return products.hashValue ^ matchingCriteria.hashValue
}
#endif
public static func ==(lhs: PurchaseRequirement, rhs: PurchaseRequirement) -> Bool {
return lhs.products == rhs.products &&
lhs.matchingCriteria == rhs.matchingCriteria &&
lhs.dependencies == rhs.dependencies
}
}