- [ between the brackets ]
- Posts
- In-App Purchases StarterKit
In-App Purchases StarterKit
How to add subscriptions to your iOS app
In 2021 Apple announced StoreKit 2, a new in-app purchase API that brings significant improvements for managing subscriptions and in-app purchases in iOS apps. In this week's newsletter, we'll take a look at StoreKit 2 and how to use it in your apps.
Key Features
Some of the features of StoreKit 2 include:
Simplified code: The API has been completely redesigned to be simpler and more Swifty. Managing in-app purchases now takes a lot less code.
Async/await: StoreKit 2 takes full advantage of async/await in Swift. This makes the code for working with in-app purchases much more readable and concise.
Real-time updates: Apps can now get real-time updates when a user's subscription status changes. This eliminates the need to manually check statuses.
Secure verification: Improved transaction verification helps ensure users get access to content they paid for. Apps can detect fraudulent transactions.
Subscription groups: Subscriptions can be grouped together, so a single purchase unlocks access to multiple different subscriptions.
Code Example
To begin, we need to create our store object
Typealiases
typealias RenewalInfo = StoreKit.Product.SubscriptionInfo.RenewalInfo
typealias RenewalState = StoreKit.Product.SubscriptionInfo.RenewalState
Two type aliases are defined here to make the code more readable and easier to manage.
Class Properties
@Published private(set) var subscriptions: [Product] = []
@Published private(set) var purchasedSubscriptions: [Product] = []
@Published private(set) var subscriptionGroupStatus: RenewalState?
private let productIds: [String] = [<product_ids>]
var updateListenerTask : Task<Void, Error>? = nil
subscriptions
: A list of available subscriptions.purchasedSubscriptions
: A list of purchased subscriptions.subscriptionGroupStatus
: The current renewal state of a subscription.productIds
: A hardcoded list of product IDs which would normally come from App Store Connect.updateListenerTask
: Holds the transaction listening task.
Initializer
init() {
updateListenerTask = listenForTransactions()
Task {
await requestProducts()
await updateCustomerProductStatus()
}
}
In the initializer, a transaction listener is set up, and it also requests available products and updates the customer's product status.
Deinitializer
deinit {
updateListenerTask?.cancel()
}
This cancels the transaction listening task when the object is deallocated.
Transaction Listener
func listenForTransactions() -> Task<Void, Error> {
return Task.detached {
//Iterate through any transactions that don't come from a direct call to `purchase()`.
for await result in Transaction.updates {
do {
let transaction = try self.checkVerified(result)
// deliver products to the user
await self.updateCustomerProductStatus()
await transaction.finish()
} catch {
print("transaction failed verification")
}
}
}
}
This function listens for transaction updates that come from the App Store. It verifies these transactions, delivers the products, and then finishes the transaction.
Requesting Products
@MainActor
func requestProducts() async {
do {
// request from the app store using the product ids (hardcoded)
subscriptions = try await Product.products(for: productIds)
print(subscriptions)
} catch {
print("Failed product request from app store server: \(error)")
}
}
requestProducts()
requests the list of available products from the App Store using the product IDs.
Making a Purchase
func purchase(_ product: Product) async throws -> Transaction? {
let result = try await product.purchase()
switch result {
case .success(let verification):
//Check whether the transaction is verified. If it isn't,
//this function rethrows the verification error.
let transaction = try checkVerified(verification)
//The transaction is verified. Deliver content to the user.
await updateCustomerProductStatus()
//Always finish a transaction.
await transaction.finish()
return transaction
case .userCancelled, .pending:
return nil
default:
return nil
}
}
purchase()
attempts to purchase a product. If successful, it updates the customer's product status and finishes the transaction.
Checking Verification
func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
//Check whether the JWS passes StoreKit verification.
switch result {
case .unverified:
//StoreKit parses the JWS, but it fails verification.
throw StoreError.failedVerification
case .verified(let safe):
//The result is verified. Return the unwrapped value.
return safe
}
}
checkVerified
checks whether a transaction is verified. It's a utility function used by other functions to ensure the transaction has gone through Apple's verification process.
Update Customer Product Status
@MainActor
func updateCustomerProductStatus() async {
for await result in Transaction.currentEntitlements {
do {
//Check whether the transaction is verified. If it isn’t, catch `failedVerification` error.
let transaction = try checkVerified(result)
switch transaction.productType {
case .autoRenewable:
if let subscription = subscriptions.first(where: {$0.id == transaction.productID}) {
purchasedSubscriptions.append(subscription)
}
default:
break
}
//Always finish a transaction.
await transaction.finish()
} catch {
print("failed updating products")
}
}
}
This function updates the list of purchased subscriptions for the customer by going through the current entitlements.
Each function in this class serves a specific purpose in the purchase and management of in-app subscriptions. The class as a whole provides a ViewModel in the MVVM design pattern, abstracting away the StoreKit 2 details from the rest of the application.
Using it, you’re able to get all of your subscription products to feed into your SwiftUI Views.
struct SubscriptionOptions: View {
@EnvironmentObject var storeVM: StoreVM
@Binding var selectedProductIndex: Int
var body: some View {
Group {
Section(header: Text("")){
HStack(spacing: 15) {
ForEach(Array(storeVM.subscriptions.enumerated()), id: \.offset) { index, product in
SubscriptionButton(index: index, isSelected: selectedProductIndex == index, action: {
selectedProductIndex = index
})
}
}
.padding(.vertical)
}
}
}
}
And thats all there is to it! StoreKit 2 is a pretty extensive library, with many more options for in-app purchases than subscriptions, but subscriptions are a good start if you’re new to making money off of your apps.
[ Zach Coriarty ]