From db5927c0ff0f4d7becf537478f4d0ceb7a0af2ea Mon Sep 17 00:00:00 2001 From: Marcel Vojtkovszky Date: Thu, 17 Oct 2024 21:02:21 +0200 Subject: [PATCH 1/3] bit nicer way of handling async context --- README.md | 5 +- Sources/StoreKitHelper/PurchaseHelper.swift | 74 ++++++++----------- .../StoreKitHelper/StoreKitCommunicator.swift | 31 ++++---- 3 files changed, 52 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index 7462b93..f1c5a67 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ enum AppProduct: String, CaseIterable, ProductRepresentable { } ``` -2. Initialize `PurchaseHelper` +2. Initialize `PurchaseHelper`, your MainView may look something like this: ```swift struct MainView: View { @@ -35,6 +35,9 @@ struct MainView: View { // this will handle fetching purchases and syncing owned purchases purchaseHelper.fetchAndSync() } + .onChange(of: purchaseHelper.purchasesReady) { newValue in + // do something when products are fetched and purchases synced. That's when it's safe to definitively evaluate if user actually has any entitlements + } } } ``` diff --git a/Sources/StoreKitHelper/PurchaseHelper.swift b/Sources/StoreKitHelper/PurchaseHelper.swift index 0a0caff..63185e6 100644 --- a/Sources/StoreKitHelper/PurchaseHelper.swift +++ b/Sources/StoreKitHelper/PurchaseHelper.swift @@ -42,9 +42,8 @@ public class PurchaseHelper: ObservableObject { self.storeKitCommunicator = StoreKitCommunicator(autoFinishTransactions: autoFinishTransactions) Task { - await storeKitCommunicator.listenForTransactionUpdatesAsync { _ in - // result can be ignored, we just wanted to finish the transaction - } + // result can be ignored, we just wanted to finish the transaction + let _ = await storeKitCommunicator.listenForTransactionUpdatesAsync() } } @@ -81,26 +80,20 @@ public class PurchaseHelper: ObservableObject { let productIds = self.allProductIds Task { - if willFetchProducts { - await storeKitCommunicator.fetchProductsAsync(productIds: productIds) { products, error in - updateUI { [weak self] in - guard let self = self else { return } - if let products { - self.products = products - self.productsFetched = true - } - } - } - } - await storeKitCommunicator.syncPurchasesAsync { ids in - updateUI { [weak self] in - guard let self = self else { return } - self.purchasedProductIds = ids - self.purchasesSynced = true - } - } + async let fetchTask: [Product]? = willFetchProducts ? storeKitCommunicator.fetchProductsAsync(productIds: productIds) : nil + async let syncTask: [String] = storeKitCommunicator.syncPurchasesAsync() + + let fetchedProducts = await fetchTask + let syncedIds = await syncTask + updateUI { [weak self] in guard let self = self else { return } + if let products = fetchedProducts { + self.products = products + self.productsFetched = true + } + self.purchasedProductIds = syncedIds + self.purchasesSynced = true self.loadingInProgress = false } } @@ -119,13 +112,12 @@ public class PurchaseHelper: ObservableObject { let productIds = self.allProductIds Task { - await storeKitCommunicator.fetchProductsAsync(productIds: productIds) { products, error in - updateUI { [weak self] in - guard let self = self else { return } - self.products = products ?? [] - self.productsFetched = true - self.loadingInProgress = false - } + let products = await storeKitCommunicator.fetchProductsAsync(productIds: productIds) + updateUI { [weak self] in + guard let self = self else { return } + self.products = products ?? [] + self.productsFetched = true + self.loadingInProgress = false } } } @@ -142,13 +134,12 @@ public class PurchaseHelper: ObservableObject { self.loadingInProgress = true Task { - await storeKitCommunicator.syncPurchasesAsync { ids in - updateUI { [weak self] in - guard let self = self else { return } - self.purchasedProductIds = ids - self.purchasesSynced = true - self.loadingInProgress = false - } + let purchasedProductIds = await storeKitCommunicator.syncPurchasesAsync() + updateUI { [weak self] in + guard let self = self else { return } + self.purchasedProductIds = purchasedProductIds + self.purchasesSynced = true + self.loadingInProgress = false } } } @@ -166,14 +157,13 @@ public class PurchaseHelper: ObservableObject { if let storeProduct = getProduct(product) { Task { - await storeKitCommunicator.purchaseAsync(product: storeProduct, options: options) { productId in - updateUI { [weak self] in - guard let self = self else { return } - if let productId, !self.purchasedProductIds.contains(productId) { - self.purchasedProductIds.append(productId) - } - self.loadingInProgress = false + let productId = await storeKitCommunicator.purchaseAsync(product: storeProduct, options: options) + updateUI { [weak self] in + guard let self = self else { return } + if let productId, !self.purchasedProductIds.contains(productId) { + self.purchasedProductIds.append(productId) } + self.loadingInProgress = false } } } else { diff --git a/Sources/StoreKitHelper/StoreKitCommunicator.swift b/Sources/StoreKitHelper/StoreKitCommunicator.swift index 3fea673..c2704fb 100644 --- a/Sources/StoreKitHelper/StoreKitCommunicator.swift +++ b/Sources/StoreKitHelper/StoreKitCommunicator.swift @@ -15,18 +15,18 @@ internal class StoreKitCommunicator { self.autoFinishTransactions = autoFinishTransactions } - func fetchProductsAsync(productIds: [String], callback: ([Product]?, Error?) -> Void) async { + func fetchProductsAsync(productIds: [String]) async -> [Product]? { do { let products = try await Product.products(for: productIds) - print("PurchaseHelper products have been fetched \(products.map({ $0.id }))") - callback(products, nil) + print("PurchaseHelper products have been fetched \(products.map { $0.id })") + return products } catch { print("PurchaseHelper Error fetching products: \(error)") - callback(nil, error) + return nil } } - func syncPurchasesAsync(callback: ([String]) -> Void) async { + func syncPurchasesAsync() async -> [String] { var newPurchasedProductIds: [String] = [] for await result in Transaction.currentEntitlements { if case .verified(let transaction) = result { @@ -37,10 +37,10 @@ internal class StoreKitCommunicator { } } print("PurchaseHelper syncPurchases complete, purchased products: \(newPurchasedProductIds)") - callback(newPurchasedProductIds) + return newPurchasedProductIds } - func purchaseAsync(product: Product, options: Set = [], callback: (String?) -> Void) async { + func purchaseAsync(product: Product, options: Set = []) async -> String? { do { let result = try await product.purchase(options: options) switch result { @@ -51,36 +51,37 @@ internal class StoreKitCommunicator { if autoFinishTransactions { await transaction.finish() } - callback(transaction.productID) + return transaction.productID case .unverified: print("PurchaseHelper transaction unverified") - callback(nil) + return nil } case .userCancelled: print("PurchaseHelper user canceled the purchase") - callback(nil) + return nil case .pending: print("PurchaseHelper purchase pending") - callback(nil) + return nil @unknown default: print("PurchaseHelper encountered an unknown purchase result") - callback(nil) + return nil } } catch { print("PurchaseHelper failed: \(error)") - callback(nil) + return nil } } - func listenForTransactionUpdatesAsync(callback: (String) -> Void) async { + func listenForTransactionUpdatesAsync() async -> String? { for await result in Transaction.updates { if case .verified(let transaction) = result { print("PurchaseHelper transaction updated outside the app: \(transaction.productID)") if autoFinishTransactions { await transaction.finish() } - callback(transaction.productID) + return transaction.productID } } + return nil } } From 542468796a9aa7ad78f5885a5382abac6a9835bc Mon Sep 17 00:00:00 2001 From: Marcel Vojtkovszky Date: Thu, 17 Oct 2024 21:44:27 +0200 Subject: [PATCH 2/3] use Swift6 --- CHANGELOG.md | 3 +++ Sources/StoreKitHelper/StoreKitCommunicator.swift | 2 +- StoreKitHelper.xcodeproj/project.pbxproj | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1931ba..037dddc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # CHANGELOG +## 0.1.2 (TBD) +* Better use of async context, `fetchAndSync()` significantly faster as a result. + ## 0.1.1 (2024-10-17) * Ensure functions intended to be public are indeed public :) * Add `autoFinishTransactions` option to init. diff --git a/Sources/StoreKitHelper/StoreKitCommunicator.swift b/Sources/StoreKitHelper/StoreKitCommunicator.swift index c2704fb..0ef8e75 100644 --- a/Sources/StoreKitHelper/StoreKitCommunicator.swift +++ b/Sources/StoreKitHelper/StoreKitCommunicator.swift @@ -8,7 +8,7 @@ import Foundation import StoreKit -internal class StoreKitCommunicator { +final internal class StoreKitCommunicator: Sendable { private let autoFinishTransactions: Bool init(autoFinishTransactions: Bool) { diff --git a/StoreKitHelper.xcodeproj/project.pbxproj b/StoreKitHelper.xcodeproj/project.pbxproj index b2cbb20..b1e0298 100644 --- a/StoreKitHelper.xcodeproj/project.pbxproj +++ b/StoreKitHelper.xcodeproj/project.pbxproj @@ -181,7 +181,7 @@ SUPPORTS_MACCATALYST = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_INSTALL_OBJC_HEADER = NO; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; XROS_DEPLOYMENT_TARGET = 2.0; }; @@ -224,7 +224,7 @@ SUPPORTS_MACCATALYST = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_INSTALL_OBJC_HEADER = NO; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; XROS_DEPLOYMENT_TARGET = 2.0; }; From 584fa8803fd3452a7f9da25a5b1b29ce797028c6 Mon Sep 17 00:00:00 2001 From: Marcel Vojtkovszky Date: Fri, 18 Oct 2024 09:11:30 +0200 Subject: [PATCH 3/3] update readme and doc --- CHANGELOG.md | 3 ++- Sources/StoreKitHelper/PurchaseHelper.swift | 13 +++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 037dddc..49afefc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ # CHANGELOG -## 0.1.2 (TBD) +## 0.1.2 (2024-10-18) * Better use of async context, `fetchAndSync()` significantly faster as a result. +* Improve documentation and examples in README.md ## 0.1.1 (2024-10-17) * Ensure functions intended to be public are indeed public :) diff --git a/Sources/StoreKitHelper/PurchaseHelper.swift b/Sources/StoreKitHelper/PurchaseHelper.swift index 63185e6..2dd1ff1 100644 --- a/Sources/StoreKitHelper/PurchaseHelper.swift +++ b/Sources/StoreKitHelper/PurchaseHelper.swift @@ -66,11 +66,12 @@ public class PurchaseHelper: ObservableObject { return products.first { $0.id == product.getId() } } - /// Will initialize and handle fetching products and syncing purchases sequentially. - /// Suggested to call this when view appears as it will guarantee `purchasesReady` to end up being `true` + /// Will initialize and handle fetching products and syncing purchases. + /// Suggested to call this when view appears as it will guarantee as it will result in `productsFetched` and `purchasesSynced` being true. + /// Note: Products will not be fetched if already fetched before, but purchases will always be re-evaluated. public func fetchAndSync() { guard !loadingInProgress else { - print("PurchaseHelper purchase is in progress, fetchAndSync() ignored") + print("PurchaseHelper loading is in progress, fetchAndSync() ignored") return } @@ -103,7 +104,7 @@ public class PurchaseHelper: ObservableObject { /// - While the process is in progress, `loadingInProgress` will be `true` public func fetchProducts() { guard !loadingInProgress else { - print("PurchaseHelper purchase is in progress, fetchProducts() ignored") + print("PurchaseHelper loading is in progress, fetchProducts() ignored") return } @@ -126,7 +127,7 @@ public class PurchaseHelper: ObservableObject { /// - While the process is in progress, `loadingInProgress` will be `true` public func syncPurchases() { guard !loadingInProgress else { - print("PurchaseHelper purchase is in progress, syncPurchases() ignored") + print("PurchaseHelper loading is in progress, syncPurchases() ignored") return } @@ -148,7 +149,7 @@ public class PurchaseHelper: ObservableObject { /// - While the process is in progress, `loadingInProgress` will be `true` public func purchase(_ product: ProductRepresentable, options: Set = []) { guard !loadingInProgress else { - print("PurchaseHelper purchase is in progress, purchase() ignored") + print("PurchaseHelper loading is in progress, purchase() ignored") return }