MerchantKit
A modern In-App Purchases management framework for iOS developers.
MerchantKit
dramatically simplifies the work indie developers have to do in order to add premium monetizable components to their applications. Track purchased products, offer auto-renewing subscriptions, restore transactions, and much more.
Designed for apps that have a finite set of purchasable products, MerchantKit
is a great way to add an unlockable ‘pro tier’ to an application, as a one-time purchase or ongoing subscription.
Example Snippets
Find out if a product has been purchased:
let product = merchant.product(withIdentifier: "iap.productidentifier")
print("isPurchased: \(merchant.state(for: product).isPurchased))"
Buy a product:
let task = merchant.commitPurchaseTask(for: purchase)
task.onCompletion = { result in
switch result {
case .succeeded(_):
print("purchase completed")
case .failed(let error):
print("\(error)")
}
}
task.start()
Get notified when a subscription expires:
<div class="highlight highlight-source-swift position-relative" data-snippet-clipboard-copy-content="public func merchant(_ merchant: Merchant, didChangeStatesFor products: Set) {
if let subscriptionProduct = products.first(where: { $0.identifier == ” subscription.protier” }) { let state = merchant.state(for: subscriptionProduct) switch state { case .isPurchased(let info): print(“subscribed, expires \(info.expiryDate)”) default: print(“does not have active subscription”) } } } “>
public func merchant(_ merchant: Merchant, didChangeStatesFor products: Set) {
if let subscriptionProduct = products.first(where: { $0.identifier == "subscription.protier" }) {
let state = merchant.state(for: subscriptionProduct)
switch state {
case .isPurchased(let info):
print("subscribed, expires \(info.expiryDate)")
default:
print("does not have active subscription")
}
}
}
Project Goals
- Straightforward, concise, API to support non-consumable, consumable and subscription In-App Purchases.
- Simplify the development of In-App Purchase interfaces in apps, including localized formatters to dynamically create strings like “£2.99 per month” or “Seven Day Free Trial”.
- No external dependencies beyond what Apple ships with iOS. The project links
Foundation
, StoreKit
, SystemConfiguration
and os
for logging purposes.
- Prioritise developer convenience and accessibility over security.
MerchantKit
users accept that some level of piracy is inevitable and not worth chasing.
- Permissive open source license.
- Compatibility with latest Swift version using idiomatic language constructs.
The codebase is in flux right now and the project does not guarantee API stability. MerchantKit
is useful, it works, and will probably save you time. That being said, MerchantKit
is by no means finished. The test suite is patchy.
Installation
CocoaPods
To integrate MerchantKit
into your Xcode project using CocoaPods, specify it in your Podfile
.
Carthage
To integrate MerchantKit
into your Xcode project using Carthage, specify it in your Cartfile
.
github "benjaminmayo/merchantkit"
Manually
Compile the MerchantKit
framework and embed it in your application. You can download the source code from Github and embed the Xcode project into your app, although you’ll have to upgrade to the latest releases manually.
Getting Started
- In your app delegate, import
MerchantKit
create a Merchant
instance in application(_:, didFinishLaunchingWithOptions:)
. Supply a configuration (such as Merchant.Configuration.default
) and a delegate.
Bool {
…
self.merchant = Merchant(configuration: .default, delegate: self)
…
}
“>
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
...
self.merchant = Merchant(configuration: .default, delegate: self)
...
}
- Register products as soon as possible (typically within
application(_:, didFinishLaunchingWithOptions:)
). You may want to load Product
structures from a file, or simply declaring them as constants in code. These constants can then be referred to statically later.
let product = Product(identifier: "iap.productIdentifier", kind: .nonConsumable)
let otherProduct = Product(identifier: "iap.otherProductIdentifier", kind: .subscription(automaticallyRenews: true))
self.merchant.register([product, otherProduct])
- Call
setup()
on the merchant instance before escaping the application(_:, didFinishLaunchingWithOptions:)
method. This tells the merchant to start observing the payment queue.
Bool {
…
self.merchant = Merchant(configuration: .default, delegate: self)
self.merchant.register(…)
self.merchant.setup()
…
}
“>
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
...
self.merchant = Merchant(configuration: .default, delegate: self)
self.merchant.register(...)
self.merchant.setup()
...
}
- Profit! Or something.
Merchant Configuration
Merchant
is initialized with a configuration object; an instance of Merchant.Configuration
. The configuration controls how Merchant
validates receipts and persist product state to storage. Most applications can simply use Merchant.Configuration.default
and get the result they expect. You can supply your own Merchant.Configuration
if you want to do something more customized.
Tip: MerchantKit
provides Merchant.Configuration.usefulForTestingAsPurchasedStateResetsOnApplicationLaunch
as a built-in configuration. This can be used to test purchase flows during development as the configuration does not persist purchase states to permanent storage. You can repeatedly test ‘buying’ any Product
, including non-consumables, simply by restarting the app. As indicated by its unwieldy name, you should not use this configuration in a released application.
Merchant Delegate
The delegate implements the MerchantDelegate
protocol. This delegate provides an opportunity to respond to events at an app-wide level. The MerchantDelegate
protocol declares a handful of methods, but only one is required to be implemented.
<div class="highlight highlight-source-swift position-relative" data-snippet-clipboard-copy-content="func merchant(_ merchant: Merchant, didChangeStatesFor products: Set) {
// Called when the purchased state of a `Product` changes.
for product in products {
print(” updated \(product)”) } } “>
func merchant(_ merchant: Merchant, didChangeStatesFor products: Set) {
// Called when the purchased state of a `Product` changes.
for product in products {
print("updated \(product)")
}
}
The delegate optionally receives loading state change events, and a customization point for handling Promoted In-App Purchase flows that were initiated externally by the App Store. Sensible default implementations are provided for these two methods.
Developers simply provide the list of products to display and tells the controller to fetch data. The delegate
notifies the app when to update its custom UI. It handles loading data, intermittent network connectivity and in-flight changes to the availability and state of products.
In addition to the renewal duration, subscriptions can include free trials and other introductory offers. You can use a SubscriptionPeriodFormatter
to format a text label in your application. If you change the free trial offer in iTunes Connect, the label will dynamically update to reflect the changed terms without requiring a new App Store binary. For example: