Skip to content

Commit c72eeb6

Browse files
1.60.39
1 parent 2b020c6 commit c72eeb6

38 files changed

+2094
-778
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// swift-tools-version: 5.9
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
6+
let package = Package(
7+
name: "StrongboxPurchases",
8+
platforms: [
9+
.iOS(.v15), .macOS(.v12)
10+
],
11+
products: [
12+
13+
.library(
14+
name: "StrongboxPurchases",
15+
targets: ["StrongboxPurchases"]
16+
)
17+
],
18+
dependencies: [
19+
20+
.package(url: "https:
21+
],
22+
targets: [
23+
24+
.target(
25+
name: "StrongboxPurchases",
26+
dependencies: [
27+
.product(name: "RevenueCat", package: "purchases-ios")
28+
]
29+
)
30+
]
31+
)
Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
import Foundation
2+
import StoreKit
3+
import RevenueCat
4+
5+
// Wrapper around RevenueCat to share code for purchases across bundles
6+
@objc public class RCStrongbox: NSObject {
7+
public enum ProductType: String {
8+
case monthly = "monthly"
9+
case yearly = "yearly"
10+
case lifetime = "lifetime"
11+
}
12+
13+
enum Entitlement: String, CaseIterable {
14+
case pro = "pro"
15+
}
16+
17+
18+
19+
private static var latestPurchaserInfo: CustomerInfo? = nil {
20+
didSet {
21+
guard latestPurchaserInfo != nil else { return }
22+
updateStatus()
23+
}
24+
}
25+
private static var currentOffering: Offering? = nil
26+
private static let syncInfoKey = "sync_info_key"
27+
private static let restoreKey = "restore_info_key"
28+
private static let defaultOfferingIdentifier = "default"
29+
30+
31+
public static var onSubscriptionUpdated: ((Bool) -> Void)?
32+
33+
public static var onFetchComplete: (() -> Void)?
34+
35+
private static var tipIdentifiers: [String] = []
36+
public static var tipProducts: [StoreProduct] = []
37+
38+
39+
@objc public static func initializeRevenueCat(
40+
key: String,
41+
tipIdentifiers: [String] = []
42+
) {
43+
Purchases.configure(
44+
withAPIKey: key,
45+
appUserID: UserIdentifier.id,
46+
purchasesAreCompletedBy: .revenueCat,
47+
storeKitVersion: .storeKit1
48+
)
49+
50+
print("🐱 Identifier \(UserIdentifier.id)")
51+
print("🐱 RevenueCat \(Purchases.shared.appUserID)")
52+
53+
self.tipIdentifiers = tipIdentifiers
54+
55+
Task {
56+
self.latestPurchaserInfo = try? await Purchases.shared.logIn(UserIdentifier.id).customerInfo
57+
self.syncPurchasesIfNeeded()
58+
self.fetchOfferings()
59+
}
60+
}
61+
62+
63+
64+
private static func fetchOfferings() {
65+
Task(priority: .userInitiated) {
66+
self.tipProducts = await Purchases.shared.products(tipIdentifiers)
67+
print("🐱 Tips Fetched: \(tipProducts)")
68+
69+
70+
do {
71+
let offerings = try await Purchases.shared.offerings()
72+
currentOffering = offerings.current ?? offerings.offering(identifier: defaultOfferingIdentifier)
73+
print("🐱 Current offering fetched: \(currentOffering?.identifier ?? "none")")
74+
} catch {
75+
print("🐱 Error fetching offerings: \(error.localizedDescription)")
76+
}
77+
78+
onFetchComplete?()
79+
}
80+
}
81+
82+
@objc public static func loadAppStoreProducts(completion: @escaping ([SKProduct]?, NSError?) -> Void) {
83+
Purchases.shared.getOfferings { offerings, error in
84+
if let error = error {
85+
print("🐱 Error fetching offerings: \(error.localizedDescription)")
86+
DispatchQueue.main.async {
87+
completion(nil, error as NSError)
88+
}
89+
return
90+
}
91+
92+
if let offerings = offerings {
93+
currentOffering = offerings.current ?? offerings.offering(identifier: defaultOfferingIdentifier)
94+
95+
if let offering = currentOffering {
96+
let products = offering.availablePackages.compactMap { $0.storeProduct.sk1Product }
97+
DispatchQueue.main.async {
98+
completion(products, nil)
99+
}
100+
} else {
101+
let error = NSError(domain: "com.strongbox.revenueCat",
102+
code: 1001,
103+
userInfo: [NSLocalizedDescriptionKey: "No offerings available"])
104+
DispatchQueue.main.async {
105+
completion(nil, error)
106+
}
107+
}
108+
} else {
109+
let error = NSError(domain: "com.strongbox.revenueCat",
110+
code: 1000,
111+
userInfo: [NSLocalizedDescriptionKey: "Failed to fetch offerings"])
112+
DispatchQueue.main.async {
113+
completion(nil, error)
114+
}
115+
}
116+
}
117+
}
118+
119+
@objc public static func availableProducts() -> [String: SKProduct] {
120+
guard let offering = currentOffering else { return [:] }
121+
122+
var products: [String: SKProduct] = [:]
123+
for package in offering.availablePackages {
124+
if let storeProduct = package.storeProduct.sk1Product {
125+
products[storeProduct.productIdentifier] = storeProduct
126+
}
127+
}
128+
129+
return products
130+
}
131+
132+
private static func getPackage(for productType: ProductType) -> Package? {
133+
guard let offering = currentOffering else { return nil }
134+
135+
136+
if let package = offering.package(identifier: productType.rawValue) {
137+
return package
138+
}
139+
140+
141+
switch productType {
142+
case .monthly:
143+
return offering.availablePackages.first(where: { $0.packageType == .monthly })
144+
case .yearly:
145+
return offering.availablePackages.first(where: { $0.packageType == .annual })
146+
case .lifetime:
147+
return offering.availablePackages.first(where: { $0.packageType == .lifetime })
148+
}
149+
}
150+
151+
@objc public static func getProduct(for productType: String) -> SKProduct? {
152+
guard let type = ProductType(rawValue: productType) else { return nil }
153+
guard let package = getPackage(for: type) else { return nil }
154+
return package.storeProduct.sk1Product
155+
}
156+
157+
158+
159+
@objc public static func isReceiptVerified() -> Bool {
160+
guard let _ = latestPurchaserInfo?.entitlements else { return false }
161+
return true
162+
}
163+
164+
@objc public static func isActive() -> Bool {
165+
guard let entitlements = latestPurchaserInfo?.entitlements else { return false }
166+
let entitlementsKeys = Entitlement.allCases.map(\.rawValue)
167+
let isActive = entitlements.active.keys.contains { entitlementsKeys.contains($0) }
168+
return isActive
169+
}
170+
171+
172+
173+
@objc public static func hasActiveYearlySubscription() -> Bool {
174+
guard let entitlements = latestPurchaserInfo?.entitlements else { return false }
175+
176+
177+
if let proEntitlement = entitlements.active[Entitlement.pro.rawValue],
178+
proEntitlement.productIdentifier.contains(ProductType.yearly.rawValue) {
179+
return true
180+
}
181+
182+
return false
183+
}
184+
185+
@objc public static func hasActiveMonthlySubscription() -> Bool {
186+
guard let entitlements = latestPurchaserInfo?.entitlements else { return false }
187+
188+
189+
if let proEntitlement = entitlements.active[Entitlement.pro.rawValue],
190+
proEntitlement.productIdentifier.contains(ProductType.monthly.rawValue) {
191+
return true
192+
}
193+
194+
return false
195+
}
196+
197+
@objc public static func hasPurchasedLifeTime() -> Bool {
198+
guard let entitlements = latestPurchaserInfo?.entitlements else { return false }
199+
200+
201+
if let proEntitlement = entitlements.active[Entitlement.pro.rawValue],
202+
proEntitlement.productIdentifier.contains(ProductType.lifetime.rawValue) {
203+
return true
204+
}
205+
206+
return false
207+
}
208+
209+
@objc public static func currentSubscriptionRenewalOrExpiry() -> Date? {
210+
guard let info = latestPurchaserInfo else { return nil }
211+
212+
213+
return info.expirationDate(forEntitlement: Entitlement.pro.rawValue)
214+
}
215+
216+
@objc public static func isFreeTrialAvailable() -> Bool {
217+
218+
let yearlyPackage = getPackage(for: .yearly)
219+
let monthlyPackage = getPackage(for: .monthly)
220+
221+
222+
if let yearlyProduct = yearlyPackage?.storeProduct.sk1Product,
223+
let intro = yearlyProduct.introductoryPrice,
224+
intro.price.compare(NSDecimalNumber.zero) == .orderedSame {
225+
return true
226+
}
227+
228+
if let monthlyProduct = monthlyPackage?.storeProduct.sk1Product,
229+
let intro = monthlyProduct.introductoryPrice,
230+
intro.price.compare(NSDecimalNumber.zero) == .orderedSame {
231+
return true
232+
}
233+
234+
return false
235+
}
236+
237+
238+
239+
@objc public static func syncPurchasesIfNeeded() {
240+
let defaults = UserDefaults.standard
241+
242+
Task(priority: .userInitiated) {
243+
if !defaults.bool(forKey: restoreKey) {
244+
print("🐱 Restoring Purchases")
245+
do {
246+
let latestInfo = try await Purchases.shared.restorePurchases()
247+
latestPurchaserInfo = latestInfo
248+
defaults.set(true, forKey: restoreKey)
249+
defaults.synchronize()
250+
} catch { }
251+
} else {
252+
print("🐱 Skipping Restoring Purchases")
253+
}
254+
if !defaults.bool(forKey: syncInfoKey) {
255+
print("🐱 Syncing Purchases")
256+
do {
257+
let latestInfo = try await Purchases.shared.syncPurchases()
258+
latestPurchaserInfo = latestInfo
259+
defaults.set(true, forKey: syncInfoKey)
260+
defaults.synchronize()
261+
} catch {
262+
print("🐱 Skipping Syncing Purchases")
263+
}
264+
}
265+
latestPurchaserInfo = (try? await Purchases.shared.customerInfo()) ?? latestPurchaserInfo
266+
}
267+
}
268+
269+
static func updateStatus() {
270+
let entitlementsKeys = Entitlement.allCases.map(\.rawValue)
271+
let isEntitled = latestPurchaserInfo?.entitlements.active.keys.contains(where: { entitlementsKeys.contains($0) })
272+
onSubscriptionUpdated?(isEntitled ?? false)
273+
}
274+
275+
276+
277+
@objc public static func restorePurchases(withCompletion completion: @escaping (NSError?) -> Void) {
278+
Purchases.shared.restorePurchases { purchaserInfo, error in
279+
DispatchQueue.main.async {
280+
if let error = error {
281+
completion(error as NSError)
282+
} else {
283+
if let info = purchaserInfo {
284+
latestPurchaserInfo = info
285+
}
286+
completion(nil)
287+
}
288+
}
289+
}
290+
}
291+
292+
293+
294+
@objc public static func purchaseProduct(_ product: SKProduct, completion: @escaping (NSError?) -> Void) {
295+
Purchases.shared.purchase(
296+
product: StoreProduct(sk1Product: product)
297+
) { (transaction, purchaserInfo, error, cancelled) in
298+
DispatchQueue.main.async {
299+
if let error = error {
300+
completion(error as NSError)
301+
} else {
302+
if let info = purchaserInfo {
303+
latestPurchaserInfo = info
304+
}
305+
completion(nil)
306+
}
307+
}
308+
}
309+
}
310+
311+
@objc public static func purchasePackage(with productType: String, completion: @escaping (NSError?) -> Void) {
312+
guard let type = ProductType(rawValue: productType),
313+
let package = getPackage(for: type) else {
314+
let error = NSError(domain: "com.strongbox.revenueCat",
315+
code: 1002,
316+
userInfo: [NSLocalizedDescriptionKey: "Package not found for product type: \(productType)"])
317+
DispatchQueue.main.async {
318+
completion(error)
319+
}
320+
return
321+
}
322+
323+
Purchases.shared.purchase(package: package) { (transaction, customerInfo, error, userCancelled) in
324+
DispatchQueue.main.async {
325+
if let error = error {
326+
completion(error as NSError)
327+
} else {
328+
if let info = customerInfo {
329+
latestPurchaserInfo = info
330+
}
331+
completion(nil)
332+
}
333+
}
334+
}
335+
}
336+
}

0 commit comments

Comments
 (0)