A Swift cross-platform package to use in-app purchase
PurchaseKit
This is a Swift cross-platform package to use in-app purchase. This framework provides a lightweight wrapper around StoreKit2.
Funcionalities
- Support only for Consumable and Non Consumable products.
- Transaction state changes are listen using the internal transaction listener to provide up-to-date content while the app is running.
- Request the products to display in your app from the App Store, using
requestProduct()
. - Purchase your in-app products from the App Store using
purchase(productId:)
. - Restore Purchase to sync user previous purchases using
restore()
. - Transaction entitlements are verifiy to unlock the purchased content.
Requirements
The latest version of PurchaseKit requires:
- Xcode 13.2+
Platform | Minimum target |
---|---|
iOS | 15.0+ |
macOS | 12.0+ |
watchOS | 8.0+ |
tvOS | 15.0+ |
Installation
Swift Package Manager
Using SPM add the following to your dependencies
‘PurchaseKit’, ‘main’, ‘https://github.com/fserrazes/PurchaseKit.git‘
How to use?
Preparations
1. Define a set of Strings that hold **ProductIds** for the products you want to sell.
ProductIds are generally in reverse domain form (“com.your-company.your-product”).
These ids will match the product ids you define in App Store Connect.
2. Add the StoreKit framework by selecting the app target, General tab.
In the **Framework, Libraries, and Embedded Content** click +
3. Create the StoreKit Configuration file in your project.
(Select File > New > File and choose the StoreKit Configuration File template).
**Note: The Product ID should match with you define set of Strings (item 1.)**
3. Add the in-app purchase capability by selecting the app target and
**Signing & Capabilities**, then click +
4. Enable StoreKit testing in Xcode (it’s disabled by default).
(Select Product > Scheme > Edit Scheme. Now select Run and the Options tab.
Than select your configuration file from the StoreKit Configuration list).
Configure the PurchaseKit service.
This is the entry point for Purchasekit.framework.
import SwiftUI
import PurchaseKit
@main
struct Main: App {
private var store: StoreViewModel
private let productIds: [String] = [com.your-company.your-product-1, com.your-company.your-product-2]
init() {
let service = Purchases.configure(with: productIds)
self.store = StoreViewModel(service: service)
}
var body: some Scene {
WindowGroup {
StoreView()
.environmentObject(store)
}
}
}
Create StoreViewModel (suggestion)
This class helps to isolate the concrete implementation from PurchaseKit (for simplicity, in this sample is partial isolated).
import SwiftUI
import PurchaseKit
final class StoreViewModel: ObservableObject {
@Published private (set) var products: [StoreProduct] = []
private var service: PurchasesProtocol
init(service: PurchasesProtocol) {
self.service = service
self.service.delegate = self
Task {
// During store initialization, request products from the App Store.
await requestProducts()
}
}
@MainActor
public func requestProducts() async {
let result = await service.requestProducts()
if case let .success(products) = result {
self.products = products
}
}
@MainActor
public func purchase(productId: String) async {
guard let result = try? await service.purchase(productId: productId) else { return }
if result, let index = products.firstIndex(where: { $0.id == productId }) {
products[index].isPurchased = result
}
}
public func restore() async {
await service.restore()
}
}
extension StoreViewModel: PurchasesDelegate {
func didFinishedPurchases(product: StoreProduct) {
DispatchQueue.main.async {
if let index = self.products.firstIndex(where: { $0.id == product.id }) {
self.products[index].isPurchased = true
// Add other update stuff here ...
}
}
}
}
Request products and Purchases
This method will return all products mapped to StoreProduct
model.
The products listed starts with purchases status equals false. The update status will be triger by didFinishedPurchases(product: StoreProduct)
delegate.
import SwiftUI
struct StoreView: View {
@EnvironmentObject private var store: StoreViewModel
var body: some View {
VStack {
ForEach(0 ..< store.products.count, id: \.self) { index in
Button(action: { Task { await store.purchase(productId: store.products[index].id) } }) {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text(store.products[index].displayName)
.font(.title2)
HStack {
Text(store.products[index].description)
.multilineTextAlignment(.leading)
Spacer()
if store.products[index].isPurchased {
Image(systemName: "checkmark.circle.fill")
.imageScale(.large)
.foregroundColor(.green)
.font(Font.title)
} else {
Text(store.products[index].displayPrice)
.foregroundColor(.white)
.frame(width: 70)
.padding(6)
.background(.blue)
.clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
}
}
}
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.disabled(product.isPurchased)
}
}
}
}
Restore Purchase
This call displays a system prompt that asks users to authenticate with their App Store credentials.
Note: Call this function only in response to an explicit user action, such as tapping a button.
import SwiftUI
struct StoreView: View {
@EnvironmentObject private var store: StoreViewModel
var body: some View {
VStack {
Button(action: {
Task {
await store.restore()
}
}) {
HStack {
Image(systemName: "arrow.triangle.2.circlepath")
Text("Restore Purchases")
}
.font(.title2)
.foregroundColor(.white)
.padding()
.frame(width: 300, height: 50, alignment: .center)
.background(Color.blue)
.cornerRadius(10.0)
}
}
}
}
Documentation
- In-App Purchase Overview (Apple)
- In-App Purchase (Apple)