Minimal StoreKit 2 helpers for subscriptions and one-off in-app purchases.
SubKit is split into two library products:
SubKit: product loading, purchasing, entitlements, subscription status, and manual sync.SubKitUI: SwiftUI-first StoreKit merchandising wrappers plus UIKit hosting helpers.
- iOS 15+
- macOS 12+
- tvOS 15+
- watchOS 8+
- visionOS 1+
SubKitUI uses StoreKit SwiftUI APIs, so its UI views are available on:
- iOS 17+
- macOS 14+
- tvOS 17+
- watchOS 10+
- visionOS 1+
Add the package to your app and import the product you need:
import SubKit
import SubKitUICreate a catalog in code. StoreKit does not expose a general API to fetch all of your App Store Connect product IDs automatically, so you provide them yourself.
import SubKit
letsubKit=SubKitClient(
configuration:Configuration(
productIDs:["com.example.subkit.lifetime","com.example.subkit.monthly","com.example.subkit.yearly",],
subscriptionGroupIDs:["5982C3D1"]))Load products as early as you want in app startup or when entering your paywall:
letproducts=tryawait subKit.loadProducts()letresult=tryawait subKit.purchase(
productID:"com.example.subkit.monthly",
appAccountToken:UUID())switch result {caselet.success(purchase):
// Send purchase.jwsRepresentation or purchase.transaction
// to your backend for server-side verification / entitlement sync.
print(purchase.productID)case.pending:
// Ask to Buy / SCA / approval flow is still pending.
breakcase.userCancelled:break}For UIKit apps, you can confirm purchases in a view controller or scene:
letresult=tryawait subKit.purchase(
productID:"com.example.subkit.lifetime",
appAccountToken:UUID(),
confirmingIn: viewController
)letresult=tryawait subKit.purchase(
productID:"com.example.subkit.lifetime",
appAccountToken:UUID(),
confirmingIn: scene
)Read the current non-consumable and subscription entitlements:
letentitlements=tryawait subKit.currentEntitlements()Each Entitlement includes:
productIDkindtransactionIDoriginalTransactionIDpurchaseDateexpirationDaterevocationDateappAccountTokentransactionjwsRepresentation
letstatuses=tryawait subKit.subscriptionStatuses(groupID:"5982C3D1")Check whether the customer is eligible for an introductory offer on an auto-renewable subscription:
letisEligible=tryawait subKit.isEligibleForIntroOffer(
productID:"com.example.subkit.monthly")StoreKit 2 keeps purchases synchronized automatically in normal cases. For the rare “restore purchases” equivalent, expose a user-driven action:
letrefreshedEntitlements=tryawait subKit.syncPurchases()SubKitUI wraps Apple’s StoreKit SwiftUI views and adds better empty/error handling for product-backed views using ContentUnavailableView.
import SubKitUI
SubKitProductView(
id:"com.example.subkit.lifetime",
appAccountToken: userID
)SubKitStoreView(
ids:["com.example.subkit.lifetime","com.example.subkit.monthly","com.example.subkit.yearly",],
appAccountToken: userID
)Preferred when you know the products:
SubKitSubscriptionStoreView(
productIDs:["com.example.subkit.monthly","com.example.subkit.yearly",],
appAccountToken: userID
)You can also use a subscription group ID:
SubKitSubscriptionStoreView(
groupID:"5982C3D1",
appAccountToken: userID
)You can also scope the store to relationship-specific flows, such as upgrades:
SubKitSubscriptionStoreView(
groupID:"5982C3D1",
appAccountToken: userID,
visibleRelationships:.upgrade
)appAccountToken is a first-class parameter on the merchandising views so StoreKit-driven purchases from those views can carry your account identifier without requiring callers to remember .inAppPurchaseOptions.
Because the wrappers are ordinary SwiftUI views around Apple's StoreKit views, you can chain StoreKit SwiftUI modifiers directly on them.
Subscription examples:
SubKitSubscriptionStoreView(groupID:"5982C3D1"){PassMarketingContent()}.storeButton(.visible, for:.redeemCode,.policies).subscriptionStorePolicyForegroundStyle(.white).subscriptionStoreButtonLabel(.multiline.displayName).subscriptionStoreControlStyle(.buttons)You can also react to StoreKit-driven purchases from descendant merchandising views:
SubKitStoreView(ids:["com.example.subkit.lifetime"]).onInAppPurchaseStart{ product inprint("Starting purchase for \\(product.id)")}.onInAppPurchaseCompletion{ product, result inprint("Completed purchase for \\(product.id): \\(result)")}Common modifiers and tasks you can use directly on SubKitUI views include:
.storeButton(..., for: .redeemCode, .policies, .restorePurchases, .signIn).subscriptionStoreSignInAction { ... }.subscriptionStorePolicyForegroundStyle(...).subscriptionStoreButtonLabel(...).subscriptionStoreControlStyle(...).subscriptionStoreControlIcon { ... }.backgroundStyle(...).productViewStyle(...).onInAppPurchaseStart { ... }.onInAppPurchaseCompletion { ... }.subscriptionStatusTask(for:).currentEntitlementTask(for:).storeProductsTask(for:)
StoreKit can present Apple’s built-in subscription-management sheet from inside your app.
If you want the call site to control presentation, create a model and toggle it:
@StatevarmanageSubscriptions=SubKitManageSubscriptionsModel(
subscriptionGroupID:"5982C3D1")Mount the presenter somewhere in the view tree:
SubKitManageSubscriptionsPresenter(model: manageSubscriptions)Then trigger it from any UI:
Button("Manage Subscription"){
manageSubscriptions.present()}Or use the built-in button wrapper:
SubKitManageSubscriptionsButton()If you want to scope the sheet to a specific subscription group:
SubKitManageSubscriptionsButton(
subscriptionGroupID:"5982C3D1")You can also give the button an external model:
SubKitManageSubscriptionsButton(model: manageSubscriptions){Label("Manage Subscription", systemImage:"gear")}You can also provide a custom label:
SubKitManageSubscriptionsButton(subscriptionGroupID:"5982C3D1"){Label("Manage Subscription", systemImage:"gear")}Host the SwiftUI wrappers in UIKit:
letcontroller=SubKitProductViewController(
productID:"com.example.subkit.lifetime",
appAccountToken: userID
)letcontroller=SubKitStoreViewController(
productIDs:["com.example.subkit.lifetime","com.example.subkit.monthly",],
appAccountToken: userID
)letcontroller=SubKitSubscriptionStoreViewController(
productIDs:["com.example.subkit.monthly","com.example.subkit.yearly",],
appAccountToken: userID
)If you need StoreKit SwiftUI modifiers in UIKit, host the SwiftUI view directly instead of using the convenience controller:
letview=SubKitSubscriptionStoreView(groupID:"5982C3D1").storeButton(.visible, for:.redeemCode,.policies).subscriptionStorePolicyForegroundStyle(.white)letcontroller=UIHostingController(rootView: view)For UIKit, the more reliable approach is to host the SwiftUI button directly and add it as a child view controller:
finalclassSettingsViewController:UIViewController{private lazy varmanageSubscriptionsButtonController=UIHostingController(
rootView:SubKitManageSubscriptionsButton(
subscriptionGroupID:"5982C3D1"){Label("Manage Subscription", systemImage:"gear")})overridefunc viewDidLoad(){
super.viewDidLoad()addChild(manageSubscriptionsButtonController)
view.addSubview(manageSubscriptionsButtonController.view)
manageSubscriptionsButtonController.view.translatesAutoresizingMaskIntoConstraints =falseNSLayoutConstraint.activate([
manageSubscriptionsButtonController.view.centerXAnchor.constraint(equalTo: view.centerXAnchor),
manageSubscriptionsButtonController.view.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant:-24),])
manageSubscriptionsButtonController.didMove(toParent:self)}}That keeps the actual StoreKit SwiftUI button in the view hierarchy, which has been much more reliable than trying to bridge manageSubscriptionsSheet through an invisible UIKit presenter.
- v1 supports auto-renewable subscriptions and non-consumables.
- Consumables are currently out of scope.
- The package does not perform backend networking for you.
- Send StoreKit transaction data or JWS to your backend after a successful purchase.
- The
productIDs-backed UI wrappers can showContentUnavailableViewfor empty and error states. - The
groupID-only subscription view still relies on Apple’sSubscriptionStoreView(groupID:), because StoreKit does not provide a direct way to enumerate every product in a subscription group from the group ID alone. visibleRelationshipsis supported on thegroupIDsubscription initializer, so upgrade, downgrade, crossgrade, and current-plan focused views are available there.SubKitSubscriptionStoreViewdoes not currently expose control over the initially selected subscription plan. On the current public StoreKit API, Apple chooses the default selection.- Many advanced StoreKit SwiftUI capabilities are available by chaining modifiers directly on the wrappers rather than through dedicated
SubKitAPIs. - The UIKit convenience controllers are intentionally thin and do not expose configuration points for SwiftUI StoreKit modifiers. Use
UIHostingControllerdirectly when you need modifier-based customization. SubKitManageSubscriptionsButtonandSubKitManageSubscriptionsPresenterpresent Apple’s subscription-management UI, which lets customers manage or cancel App Store subscriptions from inside your app.- In SwiftUI, manage-subscriptions state is owned with
@Stateand driven bySubKitManageSubscriptionsModel. - For UIKit, hosting
SubKitManageSubscriptionsButtonin aUIHostingControlleris the recommended subscription-management integration. SubKitUIfocuses on turnkey wrappers around Apple's built-in merchandising views. It does not yet provide a higher-level API for fully custom StoreKit SwiftUI paywalls built aroundstoreProductsTask,Product.CollectionTaskState, or custom subscription pickers.
SubKitUI includes #Preview blocks in Sources/SubKitUI/SubKitViews.swift for:
- product view
- store view
- subscription view