Skip to content

Commit

Permalink
Merge branch 'release/0.1.2'
Browse files Browse the repository at this point in the history
  • Loading branch information
mvojtkovszky committed Oct 18, 2024
2 parents 85c9b06 + 584fa88 commit c61a8a5
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 67 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# CHANGELOG

## 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 :)
* Add `autoFinishTransactions` option to init.
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
}
}
```
Expand Down
87 changes: 39 additions & 48 deletions Sources/StoreKitHelper/PurchaseHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}

Expand All @@ -67,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
}

Expand All @@ -81,26 +81,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
}
}
Expand All @@ -110,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
}

Expand All @@ -119,13 +113,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
}
}
}
Expand All @@ -134,21 +127,20 @@ 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
}

print("PurchaseHelper syncPurchases()")
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
}
}
}
Expand All @@ -157,7 +149,7 @@ public class PurchaseHelper: ObservableObject {
/// - While the process is in progress, `loadingInProgress` will be `true`
public func purchase(_ product: ProductRepresentable, options: Set<Product.PurchaseOption> = []) {
guard !loadingInProgress else {
print("PurchaseHelper purchase is in progress, purchase() ignored")
print("PurchaseHelper loading is in progress, purchase() ignored")
return
}

Expand All @@ -166,14 +158,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 {
Expand Down
33 changes: 17 additions & 16 deletions Sources/StoreKitHelper/StoreKitCommunicator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,25 @@
import Foundation
import StoreKit

internal class StoreKitCommunicator {
final internal class StoreKitCommunicator: Sendable {
private let autoFinishTransactions: Bool

init(autoFinishTransactions: Bool) {
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 {
Expand All @@ -37,10 +37,10 @@ internal class StoreKitCommunicator {
}
}
print("PurchaseHelper syncPurchases complete, purchased products: \(newPurchasedProductIds)")
callback(newPurchasedProductIds)
return newPurchasedProductIds
}

func purchaseAsync(product: Product, options: Set<Product.PurchaseOption> = [], callback: (String?) -> Void) async {
func purchaseAsync(product: Product, options: Set<Product.PurchaseOption> = []) async -> String? {
do {
let result = try await product.purchase(options: options)
switch result {
Expand All @@ -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
}
}
4 changes: 2 additions & 2 deletions StoreKitHelper.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down Expand Up @@ -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;
};
Expand Down

0 comments on commit c61a8a5

Please sign in to comment.