Skip to content

Commit

Permalink
Merge branch 'release/0.1.1'
Browse files Browse the repository at this point in the history
  • Loading branch information
mvojtkovszky committed Oct 17, 2024
2 parents 00449de + 220c5c2 commit 85c9b06
Show file tree
Hide file tree
Showing 9 changed files with 155 additions and 55 deletions.
96 changes: 96 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Xcode
#
DerivedData/
build/
*.xcworkspace
!default.xcworkspace
xcuserdata/
*.xcodeproj/*
!*.xcodeproj/project.pbxproj
!*.xcodeproj/xcshareddata/
!*.xcworkspace/contents.xcworkspacedata
*.xcuserstate
*.xcscmblueprint
*.swifttemplate

# Swift Package Manager
#
.build/
Packages/
Package.pins
.swiftpm/xcode/package.xcworkspace/
.swiftpm/xcode/xcshareddata/

# CocoaPods
#
Pods/
Podfile.lock
Podfile/.swift-version

# Carthage
#
Carthage/Build/

# Fastlane
#
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/tests_output

# Xcode Specific
#
*.moved-aside
*.hmap
*.ipa
*.dSYM.zip
*.dSYM
*.xcarchive
.xcresult/

# Playgrounds
#
timeline.xctimeline
playground.xcworkspace

# Shared frameworks
#
Carthage/Build
Carthage/Checkouts

# Generated Files
#
*.rpt
*.sav

# App packaging
#
*.pmdoc

# SwiftPM Binary Artifacts
.xcframeworks/

# Temporary files
#
*.tmp
*.temp
*.swp
*.swo
*.DS_Store
*.class
*.lock

# Testing
#
test_output/

# Environment specific files
#
.vscode/
.env

# Editor specific
#
.idea/
*.iml
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# CHANGELOG

## 0.1.1 (2024-10-17)
* Ensure functions intended to be public are indeed public :)
* Add `autoFinishTransactions` option to init.
* Improve documentation and examples in README.md

## 0.1.0 (2024-10-17)
* Initial Release

3 changes: 3 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import PackageDescription

let package = Package(
name: "StoreKitHelper",
platforms: [
.iOS(.v16)
],
products: [
.library(name: "StoreKitHelper", targets: ["StoreKitHelper"]),
],
Expand Down
36 changes: 19 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ A lightweight StoreKit2 wrapper designed to simplify purchases with SwiftUI.

## How do I get started

1. Define your in-app purchases (IAPs) or subscriptions by conforming to the `ProductRepresentable` protocol, as shown below:
1. Define your in-app purchases (IAPs) or subscriptions by conforming to the `ProductRepresentable` protocol, for example:

```swift
enum AppProduct: String, CaseIterable, ProductRepresentable {

case iap1 = "yourApp.purchase1"
case iap2 = "yourApp.purchase2"
case subscription1 = "yourApp.subscription1"
case subscription2 = "yourApp.subscription2"

// ProductRepresentable requires this
func getId() -> String {
rawValue
}
Expand All @@ -23,8 +23,6 @@ enum AppProduct: String, CaseIterable, ProductRepresentable {
2. Initialize `PurchaseHelper`

```swift
import StoreKitHelper

struct MainView: View {
@StateObject private var purchaseHelper = PurchaseHelper(products: AppProduct.allCases)

Expand All @@ -34,41 +32,45 @@ struct MainView: View {
}
.environmentObject(purchaseHelper)
.onAppear {
// this will handle fetching purchases and sync owned purchases
// this will handle fetching purchases and syncing owned purchases
purchaseHelper.fetchAndSync()
}
}
}
```

3. Display and handle purchases anywhere in the app.
3. Display and handle product purchases anywhere in the app.

```swift
struct PaywallView: View {
@EnvironmentObject purchaseHelper: PurchaseHelper

var body: some View {
VStack {
// get purchase info with all the product data
let purchase = purchaseHelper.getProduct(AppProduct.subscription1)

// your view presenting the product data
PurchaseInfoView(purchase)
.onTapGesture {
purchaseHelper.purchase(AppProduct.subscription1)
if let product = purchaseHelper.getProduct(AppProduct.subscription1) {
VStack {
// title and price
Text(product.displayName)
Text(product.displayPrice)
// subscribe button
Button("Subscribe") {
// init purchase flow
purchaseHelper.purchase(AppProduct.subscription1)
}
.disabled(purchaseHelper.loadingInProgress)
}
}
}
.onChange(of: purchaseHelper.loadingInProgress) { newValue in
if purchaseHelper.isPurchased(AppProduct.subscription1) {
.onChange(of: purchaseHelper.isPurchased(AppProduct.subscription1)) { isPurchased in
if isPurchased {
// Success!
}
}
}
}
```

Since `PurchaseHelper` is an `ObservableObject`, any change will automatically propagate to the views utilizing any of the public properties.
Since `PurchaseHelper` is an `ObservableObject`, any change will automatically propagate to the views utilizing any of the public properties or getters using those properties, as demonstrated in the example above.


## Is That All?
Expand All @@ -80,7 +82,7 @@ For more advanced configurations and details, check out the public properties an

## Installation

To integrate `StoreKitHelper` into your project, add it via Swift Package Manager or manually.
To integrate it into your project, add it via Swift Package Manager or manually.

### Swift Package Manager
1. In Xcode, select `File > Add Packages`.
Expand Down
1 change: 1 addition & 0 deletions Sources/StoreKitHelper/ProductRepresentable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
//

public protocol ProductRepresentable {
/// Identifier for a product
func getId() -> String
}
31 changes: 17 additions & 14 deletions Sources/StoreKitHelper/PurchaseHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,19 @@ public class PurchaseHelper: ObservableObject {
@Published private var products: [Product] = [] // StoreKit products

private let allProductIds: [String]
private let storeKitCommunicator = StoreKitCommunicator()
private let storeKitCommunicator: StoreKitCommunicator


/// Initialize helper
/// - Parameters:
/// - productIds: all product ids supported by the app.
/// - listenForUpdates: if set to true, listen for transaction updates otside of the app. this can generally be omitted If you call `fetchAndSync` every time main view appears,
init(products: [ProductRepresentable]) {
/// - products: all product ids supported by the app.
/// - autoFinishTransactions: call `Transaction.finish()` on verified transactions. `true` by default. Let it be unless you verify your transaction on own backend.
public init(
products: [ProductRepresentable],
autoFinishTransactions: Bool = true
) {
self.allProductIds = products.map { $0.getId() }
self.storeKitCommunicator = StoreKitCommunicator(autoFinishTransactions: autoFinishTransactions)

Task {
await storeKitCommunicator.listenForTransactionUpdatesAsync { _ in
Expand All @@ -45,7 +49,7 @@ public class PurchaseHelper: ObservableObject {
}

/// Determine if a given product has been purchased.
func isPurchased(_ product: ProductRepresentable) -> Bool {
public func isPurchased(_ product: ProductRepresentable) -> Bool {
guard purchasesSynced else {
print("PurchaseHelper purchases not synced yet. Call syncPurchases() first")
return false
Expand All @@ -55,17 +59,17 @@ public class PurchaseHelper: ObservableObject {

/// Get `StoreKit`product for given app product.
/// Make sure `fetchAndSync` or `fetchProducts` is called before that
func getProduct(_ product: ProductRepresentable) -> Product? {
public func getProduct(_ product: ProductRepresentable) -> Product? {
guard productsFetched else {
print("PurchaseHelper products not fetched yet. Call fetchProducts() first")
return nil
}
return products.first { $0.id == product.getId() }
}

/// Will initialize and handle fetching products and syncing purchases
/// 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`
func fetchAndSync() {
public func fetchAndSync() {
guard !loadingInProgress else {
print("PurchaseHelper purchase is in progress, fetchAndSync() ignored")
return
Expand Down Expand Up @@ -102,9 +106,9 @@ public class PurchaseHelper: ObservableObject {
}
}

/// Fetches products from the store.
/// Fetches products from the store. You can then retrieve a product by calling `getProduct()`
/// - While the process is in progress, `loadingInProgress` will be `true`
func fetchProducts() {
public func fetchProducts() {
guard !loadingInProgress else {
print("PurchaseHelper purchase is in progress, fetchProducts() ignored")
return
Expand All @@ -128,7 +132,7 @@ public class PurchaseHelper: ObservableObject {

/// Synch owned purchases (entitlements) from the store
/// - While the process is in progress, `loadingInProgress` will be `true`
func syncPurchases() {
public func syncPurchases() {
guard !loadingInProgress else {
print("PurchaseHelper purchase is in progress, syncPurchases() ignored")
return
Expand All @@ -149,10 +153,9 @@ public class PurchaseHelper: ObservableObject {
}
}

/// Init purchase of product
/// - Before the process starts, `purchaseFlowSuccess` will be set to `false` and set to `true` only upon successful purchase.
/// Init purchase of a given product, with optionally provided `options`
/// - While the process is in progress, `loadingInProgress` will be `true`
func purchase(_ product: ProductRepresentable, options: Set<Product.PurchaseOption> = []) {
public func purchase(_ product: ProductRepresentable, options: Set<Product.PurchaseOption> = []) {
guard !loadingInProgress else {
print("PurchaseHelper purchase is in progress, purchase() ignored")
return
Expand Down
17 changes: 14 additions & 3 deletions Sources/StoreKitHelper/StoreKitCommunicator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import Foundation
import StoreKit

internal class StoreKitCommunicator {
private let autoFinishTransactions: Bool

init(autoFinishTransactions: Bool) {
self.autoFinishTransactions = autoFinishTransactions
}

func fetchProductsAsync(productIds: [String], callback: ([Product]?, Error?) -> Void) async {
do {
Expand All @@ -25,8 +30,10 @@ internal class StoreKitCommunicator {
var newPurchasedProductIds: [String] = []
for await result in Transaction.currentEntitlements {
if case .verified(let transaction) = result {
if autoFinishTransactions {
await transaction.finish()
}
newPurchasedProductIds.append(transaction.productID)
await transaction.finish()
}
}
print("PurchaseHelper syncPurchases complete, purchased products: \(newPurchasedProductIds)")
Expand All @@ -41,7 +48,9 @@ internal class StoreKitCommunicator {
switch verification {
case .verified(let transaction):
print("PurchaseHelper transaction verified for \(transaction.productID)")
await transaction.finish()
if autoFinishTransactions {
await transaction.finish()
}
callback(transaction.productID)
case .unverified:
print("PurchaseHelper transaction unverified")
Expand All @@ -67,8 +76,10 @@ internal class StoreKitCommunicator {
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)
await transaction.finish()
}
}
}
Expand Down

This file was deleted.

This file was deleted.

0 comments on commit 85c9b06

Please sign in to comment.