π Learn how to build and use this package: https://www.swiftful-thinking.com/offers/REyNLwwH
A reusable PurchaseManager for Swift applications, built for Swift 6. PurchaseManager wraps a PurchaseService implementation (Mock, StoreKit, RevenueCat) through a single API. Includes @Observable support.
Details (Click to expand)
Add SwiftfulPurchasing to your project.
https://github.com/SwiftfulThinking/SwiftfulPurchasing.git
Import the package.
import SwiftfulPurchasingCreate an instance of PurchaseManager with a PurchaseService:
#if DEBUG
let purchaseManager = PurchaseManager(service: MockPurchaseService(), logger: logManager)
#else
let purchaseManager = PurchaseManager(service: RevenueCatPurchaseService(apiKey: apiKey), logger: logManager)
#endifOptionally add to the SwiftUI environment:
Text("Hello, world!")
.environment(purchaseManager)Details (Click to expand)
PurchaseManager is initialized with a PurchaseService. This is a public protocol you can use to create your own dependency.
Pre-built implementations:
- Mock β included, for SwiftUI previews and testing
- StoreKit β included, uses StoreKit 2 framework directly
- RevenueCat β SwiftfulPurchasingRevenueCat
StoreKitPurchaseService is included within the package:
let purchaseManager = PurchaseManager(service: StoreKitPurchaseService(), logger: logManager)MockPurchaseService is included for SwiftUI previews and testing:
// No activeEntitlements = the user has not purchased
let service = MockPurchaseService(activeEntitlements: [], availableProducts: AnyProduct.mocks)
// Yes activeEntitlements = the user has purchased
let service = MockPurchaseService(activeEntitlements: [PurchasedEntitlement.mock], availableProducts: AnyProduct.mocks)You can create your own PurchaseService by conforming to the protocol:
public protocol PurchaseService: Sendable {
func getProducts(productIds: [String]) async throws -> [AnyProduct]
func getUserEntitlements() async throws -> [PurchasedEntitlement]
func purchaseProduct(productId: String) async throws -> [PurchasedEntitlement]
func checkTrialEligibility(productId: String) async throws -> Bool
func restorePurchase() async throws -> [PurchasedEntitlement]
func listenForTransactions(onTransactionsUpdated: @escaping @Sendable ([PurchasedEntitlement]) async -> Void) async
func logIn(userId: String) async throws -> [PurchasedEntitlement]
func updateProfileAttributes(attributes: PurchaseProfileAttributes) async throws
func logOut() async throws
}Details (Click to expand)
The manager automatically fetches and listens for purchased entitlements on launch.
Call logIn when the userId is set or changes. You can call logIn every app launch.
try await purchaseManager.logIn(userId: "user_123")
try await purchaseManager.logIn(userId: "user_123", userAttributes: PurchaseProfileAttributes(email: "hello@example.com"))
try await purchaseManager.logOut()Optionally update profile attributes after login:
try await purchaseManager.updateProfileAttributes(attributes: PurchaseProfileAttributes(
email: "hello@example.com",
mixpanelDistinctId: mixpanelId,
firebaseAppInstanceId: firebaseId
))Details (Click to expand)
purchaseManager.entitlements // all purchased entitlements
purchaseManager.entitlements.active // all purchased entitlements that are still active
purchaseManager.entitlements.hasActiveEntitlement // user has at least 1 active entitlementlet products = try await purchaseManager.getProducts(productIds: ["product.yearly", "product.monthly"])let entitlements = try await purchaseManager.purchaseProduct(productId: "product.yearly")let entitlements = try await purchaseManager.restorePurchase()let isEligible = try await purchaseManager.checkTrialEligibility(productId: "product.yearly")- Non-subscription (non-recurring) purchases β one-time purchases, consumables, etc.
This package includes a .claude/swiftful-purchasing-rules.md with usage guidelines, purchase flow patterns, and integration advice for projects using Claude Code.
- iOS 17.0+
- macOS 14.0+
SwiftfulPurchasing is available under the MIT license.