InAppPurchaseKit provides developers for Apple platforms with the ability to add a subscription screen to their apps. It also features an optional tip jar screen too and other views to show locked content. This is built using SwiftUI so can be displayed natively from a SwiftUI app or using a UIHostingController in a UIKit app.
InAppPurchaseKit is built on top of StoreKit 2 and is fully compliant with Apple Developer Guidelines. It has already been used in multiple production apps with no issues.
Before using InAppPurchaseKit, please ensure you have setup your in-app purchases in App Store Connect.
- Requirements
- Integration
- Usage
- InAppPurchaseKit
- InAppPurchaseKitConfiguration
- PurchaseFeature
- PurchaseTiers
- PurchaseTierConfiguration
- LegacyPurchaseTierConfiguration
- LegacyPurchaseThreshold
- TipJarTiers
- PurchaseTierConfiguration
- LockedFeatureConfiguration
- ProductsLoadState
- PurchaseState
- InAppPurchaseView
- LockedInAppPurchaseFeatureButton
- LockedInAppPurchaseFeatureNavigationLink
- LockedInAppPurchaseFeatureRow
- LockedInAppPurchaseFeatureView
- LockedInAppPurchaseWidgetView
- InAppPurchaseSettingsRow
- TipJarView
- Other Packages
- iOS/iPadOS 17.0+
- macOS 14.4+
- tvOS 17.0+
- visionOS 1.0+
- watchOS 10.0+
- Xcode 17.0+
InAppPurchaseKit can be added to your app via Swift Package Manager in Xcode. Add to your project like so:
dependencies: [
.package(url: "https://github.com/adamfootdev/InAppPurchaseKit.git", from: "4.0.0")
]To start using the framework, you'll need to import it first:
import InAppPurchaseKitInAppPurchaseKit can be accessed through the Observable class InAppPurchaseKit. You need to configure it during app launch, like so:
let inAppPurchase = InAppPurchaseKit.configure(
with: configuration
)As it is Observable, you can monitor it as a @State value for updating features in your app.
This is a struct containing all of the relevant details required to configure InAppPurchaseKit. It can be created like so:
let configuration = InAppPurchaseKitConfiguration(
"My App Pro",
subtitle: "Unlock all features.",
appName: "My App",
imageName: "app_icon",
systemImage: "plus.app",
tintColor: .green,
termsOfUseURL: URL(string: "https://example.com/terms-of-use")!,
privacyPolicyURL: URL(string: "https://example.com/privacy-policy")!,
features: [feature],
tiers: tiers,
legacyPurchaseThreshold: nil,
tipJarTiers: tipJarTiers,
sharedUserDefaults: .init(suiteName: "group.com.example.MyApp")!
) { product in
print("Purchased \(product.displayName)")
} onUpdatedPurchases: {
print("Updated Purchases")
}This is a struct containing details about the features available. It can be created like so:
let feature = PurchaseFeature(
title: "Feature",
description: "About this feature.",
systemImage: "checkmark",
systemColor: .blue
)This is a struct containing details about the subscription tiers. It can be created like so:
let tiers = PurchaseTiers(
weeklyTier: nil,
monthlyTier: monthlyConfiguration,
yearlyTier: yearlyConfiguration,
lifetimeTier: lifetimeConfiguration
)This is a struct containing details about an individual tier. It can be created like so:
let configuration = PurchaseTierConfiguration(
id: "com.example.MyApp.Pro.Yearly",
alternateIDs: ["com.example.MyApp.Pro.OldYearly"],
alwaysVisible: true,
isPrimary: true,
legacyConfiguration: legacyConfiguration
)This is a struct containing details about a tier used for grandfathering in existing users when switching from paid to freemium. It can be created like so:
let configuration = LegacyPurchaseTierConfiguration(
id: "com.example.MyApp.Pro.LegacyYearly",
visible: true
)This is a struct containing details about when an app switched from paid to freemium so existing users can be grandfathered in or shown a discounted in-app purchase. It can be created like so:
let threshold = LegacyPurchaseThreshold(
buildNumber: 100,
version: "2.0"
)This is a struct containing details about the tip jar tiers if available. It can be created like so:
let tiers = TipJarTiers(
smallTier: smallConfiguration,
mediumTier: mediumConfiguration,
largeTier: largeConfiguration,
hugeTier: hugeConfiguration
)This is a struct containing details about an tip jar tier. It can be created like so:
let configuration = TipJarTierConfiguration(
id: "com.example.MyApp.Tip.Small"
)This is a struct containing details about a locked feature. It can be created like so:
let configuration = LockedFeatureConfiguration(
"Title",
systemImage: "app",
titleColor: .primary,
enableIfLegacy: false
) {
print("Purchased")
}This is an enum that is used for indicating the current product load state. It can be accessed like so:
InAppPurchaseKit.shared.productsLoadStateIt features the following states:
case .pendingThis is the state before InAppPurchaseKit has loaded the in-app purchases.
case .loadingThis state indicates InAppPurchaseKit is currently loading in-app purchases.
case .loaded(
_ products: [Product],
_ introOffers: [Product: Product.SubscriptionOffer],
_ purchasedTiers: Set<PurchaseTier>,
_ legacyUser: Bool
)This state indicates that InAppPurchaseKit has finished loading. It contains available products, any intro offers (InAppPurchaseKit only supports free trials currently), tiers the user has purchased and whether they are a legacy user.
This is an enum that is used for indicating the current purchase state for the user. It also works for widgets/other extensions by checking UserDefaults. It can be accessed like so:
InAppPurchaseKit.shared.purchaseStateIt features the following states:
case .pending
case .notPurchased
case .purchasedThis is the method that should be used to determine if a user has purchased the subscription. It does not return purchased if the user is a legacy user, you should use the product load state for that.
This is a view containing all of the in-app purchase options. It can be created like so:
InAppPurchaseView(
includeNavigationStack: true,
includeDismissButton: true,
onPurchase onPurchaseAction: nil
)It has options to include its own navigation stack and dismiss button which is useful for when displaying in a sheet instead of in an existing navigation stack. A separate purchase action can be added too.
This is a view containing a button that performs an action when a user is subscribed or is locked when not. It can be created like so:
LockedInAppPurchaseFeatureButton(
configuration: configuration,
action: action
)It includes configuration options and the action to perform when subscribed. It looks best in a list.
This is a view containing a navigation link that shows a view when a user is subscribed or is locked when not. It can be created like so:
LockedInAppPurchaseFeatureNavigationLink(
configuration: configuration,
destination: destination
)It includes configuration options and the view to show when subscribed. It looks best in a list.
This is a view containing a custom view when a user is subscribed or is locked when not. It can be created like so:
LockedInAppPurchaseFeatureRow(
configuration: configuration,
content: content
)It includes configuration options and the view to display when subscribed. It looks best in a list.
This is a view that displays the primary tier and the ability to show the in-app purchase sheet. It can be created like so:
LockedInAppPurchaseFeatureView(
onPurchase: onPurchaseAction
)It includes an action to perform when purchased.
This is a view that displays a locked view on a widget. It can be created like so:
LockedInAppPurchaseWidgetView(
configuration: configuration,
learnMoreURL: URL(string: "my://subscribe")!,
tint: .green
)It includes configuration options, a URL to open in the app when selected and an optional tint color. It will adapt to different widget families.
This is a view that displays the in-app purchase name and the ability to show the in-app purchase sheet if not subscribed or a subscribed message if the user is subscribed. It can be created like so:
InAppPurchaseSettingsRow(
onPurchase: onPurchaseAction
)It includes an action to perform when purchased. It looks best in a list.
This is a view containing all of the tip jar tiers. It can be created like so:
TipJarView(
includeNavigationStack: true,
includeDismissButton: true
)It has options to include its own navigation stack and dismiss button which is useful for when displaying in a sheet instead of in an existing navigation stack.
Add an about screen to your app.
Add a features list screen to your app.
Add a help screen to your app.
Add haptic feedback to your app.
