Skip to content

Commit

Permalink
Update getting started guide
Browse files Browse the repository at this point in the history
  • Loading branch information
danielsaidi committed Aug 29, 2022
1 parent 346fd4d commit ec8d84c
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ open class StandardStorePurchaseService: StorePurchaseService {

- Parameters:
- productIds: The IDs of the products to fetch.
- context: The store context to sync with.
- context: The store context to sync with.
*/
public init(
productIds: [String],
Expand Down
160 changes: 129 additions & 31 deletions Sources/StoreKitPlus/StoreKitPlus.docc/Getting-Started.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,105 +5,195 @@ StoreKitPlus is a Swift-based library that adds extra functionality for working
StoreKitPlus builds on the great foundation that is provided by StoreKit 2 and just aims to make it easier to use StoreKit 2 in e.g. SwiftUI.


## Services

## Getting products
StoreKitPlus has a bunch of service protocols and classes that help you integrate with StoreKit.

To get products from StoreKit 2, you can use the `Product.products` api:
* ``StoreProductService`` can be implemented by classes that can be used to retrieve StoreKit products.
* ``StorePurchaseService`` can be implemented by classes that can be used to perform StoreKit product purchase operations.
* ``StoreSyncService`` can be implemented by classes that can be used to sync StoreKit purchase and product information.

There is also a ``StoreService`` protocol that implements all these protocols, for when you want a single service to do everything.

The ``StandardStoreProductService``, ``StandardStorePurchaseService`` and ``StandardStoreService`` service classes implement these protocols and can be subclassed in case you want to customize the standard behavior.

This abstract service layer lets you communicate with StoreKit in a way that makes it possible to customize functionality, mock services in unit tests, etc. It also lets you reuse functionality for handling purchases and transactions, which otherwise can become complicated.



## Observable state

StoreKitPlus has an observable ``StoreContext`` class that can be used to observe the state for a certain app. It lets you keep track of available and purchased products and can be injected into services to automatically keep the context in sync.

For instance, injecting a context into a ``StandardStoreService`` and calling ``StoreSyncService/syncStoreData()`` will automatically write products and transactions to the context:

```swift
let productIds = ["com.your-app.productid"]
let context = StoreContext()
let service = StandardStoreService(
productIds: productIds,
context: context
)
try await Product.syncStoreData()
```

Although StoreKit products and transactions are not codable, the context will persist fetched and purchased product IDs, which means that you can use local product representations (read more further down) to keep track of products and purchases even if your app fails to communicate with StoreKit, for instance when it's offline.



## Fetching products

To fetch products with StoreKit 2, you can use `StoreKit.Product.products`:

```swift
let productIds = ["com.your-app.productid"]
let products = try await Product.products(for: productIds)
```

However, if you need to fetch products in an abstract way, for instance if you need to mock this functionality in unit tests, inject additional functionality, etc., you can use the StoreKitPlus ``StoreService``, which has a ``StoreService/getProducts()`` function:
However, you can also use the StoreKitPlus ``StoreProductService`` protocol and its implementations to fetch products:

```swift
let productIds = ["com.your-app.productid"]
let context = StoreContext()
let service = StandardStoreProductService(
productIds: productIds,
context: context
)
let products = try await service.getProducts()
```

The ``StandardStoreService`` service implementation communicates directly with StoreKit and syncs the result to a provided, observable ``StoreContext``. Read more on this context further down.
The standard service implementations communicate directly with StoreKit and sync the result to the provided ``StoreContext``.



## Purchasing products

To purchase products with StoreKit 2, you can use the `Product.purchase` api:
To purchase products with StoreKit 2, you can use `StoreKit.Product.purchase`:

```swift
let result = try await product.purchase()
switch result {
case .success(let result): try await handleTransaction(result)
case .success(let result): try await handleTransaction(result) // This can become complicated
case .pending: break
case .userCancelled: break
@unknown default: break
}
return result
```

However, if you need to purchase products in an abstract way, as described abovethe StoreKitPlus ``StoreService`` protocol has a ``StoreService/purchase(_:)`` function:
However, purchases involve a bunch of steps and can become pretty complicated. To make things easier, you can use the StoreKitPlus ``StorePurchaseService`` protocol and its implementations to purchase products:

```swift
let productIds = ["com.your-app.productid"]
let context = StoreContext()
let service = StandardStorePurchaseService(
productIds: productIds,
context: context
)
let result = try await service.purchase(product)
```

The ``StandardStoreService`` service implementation communicates directly with StoreKit and syncs the result to a provided, observable ``StoreContext``. Read more on this context further down.
The standard service implementations communicate directly with StoreKit and sync the result to the provided ``StoreContext``.



## Restoring purchases

To restore purchase with StoreKit 2, you can use the `Transaction.latest(for:)` api and verify each transaction to see that it's purchased, not expired and not revoked.
To restore purchase with StoreKit 2, you can use `StoreKit.Transaction.latest(for:)` and verify each transaction to see that it's purchased, not expired and not revoked.

This involves a bunch of steps, which makes the operation pretty complicated. To simplify, you can use the ``StoreService/restorePurchases()`` function:
However, much like with purchases, transactions involve a bunch of steps and can become pretty complicated. To make things easier, you can use the StoreKitPlus ``StorePurchaseService`` protocol and its implementations to restore purchases:

```swift
let productIds = ["com.your-app.productid"]
let context = StoreContext()
let service = StandardStorePurchaseService(
productIds: productIds,
context: context
)
try await service.restorePurchases()
```

The ``StandardStoreService`` service implementation communicates directly with StoreKit and syncs the result to a provided, observable ``StoreContext``. Read more on this context further down.
The standard service implementations communicate directly with StoreKit and sync the result to the provided ``StoreContext``.



## Syncing store data

To perform a full product information sync with StoreKit 2, you can fetch products and transactions from StoreKit.
To perform a full product and purchase sync with StoreKit 2, you can fetch products and transactions from StoreKit.

This involves a bunch of steps, which makes the operation pretty complicated. To simplify, you can use the ``StoreService/syncStoreData()`` function:
However, much like with purchases, this involves a bunch of steps and can become pretty complicated. To make things easier, you can use the StoreKitPlus ``StoreSyncService`` protocol and its implementations to sync store information:

```swift
let productIds = ["com.your-app.productid"]
let context = StoreContext()
let service = StandardStoreService(
productIds: productIds,
context: context
)
try await service.syncStoreData()
```

The ``StandardStoreService`` service implementation communicates directly with StoreKit and syncs the result to a provided, observable ``StoreContext``. Read more on this context further down.
The standard service implementation communicates directly with StoreKit and syncs the result to the provided ``StoreContext``.



## Observable state
## Syncing store data on app launch

StoreKitPlus has an observable ``StoreContext`` that can be used to observe the store state for a certain app.
To sync data when the app launches or is waken up from the background, you can make your app listen to the scene phase change. In SwiftUI, this can be implemented like this:

```swift
let productIds = ["com.your-app.productid"]
let context = StoreContext()
let service = StandardStoreService(
productIds: productIds,
context: context
)
@main
struct MyApp: App {

@StateObject
private var storeContext = StoreContext()

@Environment(\.scenePhase)
private var scenePhase

var body: some Scene {
WindowGroup {
RootView()
.onChange(of: scenePhase, perform: syncStoreData)
.environmentObject(storeContext)
}
}
}

private extension MyApp {

// I use a service provider to resolve services, but you
// can create a service directly or use a way that suits
// your app. Let's just create one directly in this demo:
var storeService: StoreService {
let productIds = ["com.your-app.productid"]
let service = StandardStoreService(
productIds: productIds,
context: storeContext
)
}

func syncStoreData(for phase: ScenePhase) {
guard phase == .active else { return
Task {
try await storeService.syncStoreData()
}
}
}
```

The context lets you keep track of available and purchased products, and can be injected into a ``StandardStoreService`` to keep track of changes as the user uses the service to communicate with StoreKit.
Since the standard implementations automatically sync changes with the provided context, injecting the context as an environment object will make the global state available to the entire app.



## Local products
## Local product representations

If you want to have a local representation of your StoreKit product collection, you can use the ``ProductRepresentable`` protocol.
If you want to have a local representation of your StoreKit products, you can use the ``ProductRepresentable`` protocol, which is an easy way to provide identifiable product types that can be matched with real product IDs.

The protocol is just an easy way to provide identifiable product types, that can be matched with the real product IDs, for instance:
For instance, here we define an app-specific product, where the id:s reflect the real product id:s that are defined in App Store Connect:

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

case premiumMonthly = "com.myapp.products.premium.monthly"
case premiumYearly = "com.myapp.products.premium.yearly"
Expand All @@ -115,20 +205,28 @@ enum MyProduct: CaseIterable, String, ProductRepresentable {
You can now use this collection to initialize a standard store service:

```swift
let products = MyProduct.allCases
let products = AppProduct.allCases
let context = StoreContext()
let service = StandardStoreService(
products: products,
context: context
)
```

You can also match any product collection with a context's purchased product IDs:
You can also match any product collection with a context's purchased product id:s:

```swift
let products = MyProduct.allCases
let products = AppProduct.allCases
let context = StoreContext()
let purchased = products.purchased(in: context)
```

However, some operations require that you provide a real StoreKit `Product`.
One benefit of using local product representations, is that since the context will persist the id:s of fetched and purchased products, you will be able to check if a product has been purchased or not, even if the app fails to fetch StoreKit transactions.

However, some operations require that you provide a real StoreKit `Product`, for instance purchasing a product. As such, you may want to display a loading indicator over your purchase buttons if the app has not yet fetched products from StoreKit.



## StoreKit configuration files

StoreKitPlus is just a small layer on top of StoreKit, which means that all the amazing StoreKit tools provided by Xcode works as expected. For instance, StoreKit configuration files work just like when you just use the native StoreKit api.

0 comments on commit ec8d84c

Please sign in to comment.