Add in-app purchases to your SwiftUI app in minutes, not hours. No StoreKit complexity, just simple code that works.
๐ InAppKit - Because in-app purchases shouldn't be complicated.
- โจ Features
- ๐ง Requirements
- ๐ฆ Installation
- ๐ Quick Start
- ๐ก Real-World Examples
- ๐ Core Concepts
- ๐ฏ Choose Your App's Monetization Pattern
- ๐ Type-Safe Premium Gating
- ๐จ Paywall & UI Customization
- ๐๏ธ Architecture
- ๐ฏ Advanced Features
- ๐ ๏ธ Troubleshooting
- ๐ Privacy & Security
- ๐ค Contributing
- ๐ License
- ๐ Simple Setup - Add
.withPurchases()
to any view and you're done - ๐ฏ Smart Gating - Use
.requiresPurchase()
on any button or view - ๐จ Beautiful Paywalls - Professional upgrade screens included
- โก Zero Config - Works with App Store Connect automatically
- ๐ Handles Everything - Purchases, restoration, validation - all automatic
- ๐ฑ Works Everywhere - iOS, macOS, watchOS, tvOS
- ๐จ Ready-to-Use UI - Premium badges and upgrade flows included
- iOS 17.0+ / macOS 15.0+ / watchOS 10.0+ / tvOS 17.0+
- Xcode 15.0+
- Swift 6.1+
Add InAppKit to your project using Xcode:
- Go to File โ Add Package Dependencies
- Enter the repository URL:
https://github.com/tddworks/InAppKit.git
- Select Up to Next Major Version starting from
1.0.0
Or add it to your Package.swift
:
dependencies: [
.package(url: "https://github.com/tddworks/InAppKit.git", from: "1.0.0")
]
ContentView()
.withPurchases("com.yourapp.pro")
Or with Product array if you need features:
ContentView()
.withPurchases(products: [Product("com.yourapp.pro")])
Button("Premium Feature") { doPremiumThing() }
.requiresPurchase()
That's it! ๐ InAppKit handles the rest automatically.
๐ Define specific features
enum AppFeature: String, InAppKit.AppFeature {
case removeAds = "remove_ads"
case cloudSync = "cloud_sync"
case exportPDF = "export_pdf"
}
ContentView()
.withPurchases(products: [
Product("com.yourapp.pro", AppFeature.allCases)
])
๐จ Customize the paywall
ContentView()
.withPurchases("com.yourapp.pro")
.withPaywall { context in
Text("Unlock \(context.triggeredBy ?? "premium features")")
// Your custom paywall UI here
}
๐ฏ Smart conditional upgrades
Button("Save Document") { save() }
.requiresPurchase(AppFeature.cloudSync, when: documentCount > 5)
Button("Export Photo") { exportPhoto() }
.requiresPurchase()
Result: User sees upgrade screen when they try to export
Button("Save Note") { saveNote() }
.requiresPurchase(when: noteCount > 50)
Result: After 50 notes, upgrade prompt appears
Button("Export for Client") { exportForClient() }
.requiresPurchase(AppFeature.clientTools)
Result: Business users see relevant upgrade options
1. Products - What users can buy 2. Features - What gets unlocked
// Product: "Pro Version"
// Features: No ads, cloud sync, export
Product("com.app.pro", [.noAds, .cloudSync, .export])
InAppKit adapts to how your users think about value, not just technical features:
Perfect for: Apps where users need to experience value first
// Users get core functionality, pay for advanced features
Product("com.photoapp.pro", [AppFeature.advancedFilters, AppFeature.cloudStorage])
User Mental Model: "I love this app, now I want more powerful features"
- โ Users understand the upgrade value
- โ Natural conversion from free to paid
- โ Low barrier to entry
Perfect for: Different user types with different needs
// Starter: Casual users
Product("com.designapp.starter", [AppFeature.basicTemplates, AppFeature.export])
// Professional: Power users
Product("com.designapp.pro", [AppFeature.premiumTemplates, AppFeature.advancedExport, AppFeature.teamSharing])
// Enterprise: Teams & organizations
Product("com.designapp.enterprise", AppFeature.allCases)
User Mental Model: "I know what level of user I am, show me my tier"
- โ Clear value differentiation
- โ Room for users to grow
- โ Predictable pricing psychology
Perfect for: Specialized workflows and use cases
// Content Creator Pack
Product("com.videoapp.creator", [AppFeature.advancedEditing, AppFeature.exportFormats, AppFeature.musicLibrary])
// Business Pack
Product("com.videoapp.business", [AppFeature.branding, AppFeature.analytics, AppFeature.teamWorkspace])
User Mental Model: "I need tools for my specific workflow"
- โ Solves complete user problems
- โ Higher perceived value
- โ Targets specific personas
Perfect for: Services that provide continuous value
// Monthly: Try it out
Product("com.cloudapp.monthly", [AppFeature.cloudSync, AppFeature.prioritySupport])
// Annual: Committed users
Product("com.cloudapp.annual", [AppFeature.cloudSync, AppFeature.prioritySupport, AppFeature.advancedFeatures])
User Mental Model: "I'm paying for ongoing service and updates"
- โ Matches recurring value delivery
- โ Lower monthly commitment
- โ Incentivizes annual savings
InAppKit uses a fluent chainable API for clean, readable configuration:
ContentView()
.withPurchases(products: [Product("com.app.pro", AppFeature.allCases)])
.withPaywall { context in CustomPaywall(context) }
.withTerms { TermsView() }
.withPrivacy { PrivacyView() }
InAppKit includes a beautiful, modern paywall out of the box:
// Use default paywall with fluent API
ContentView()
.withPurchases(products: products)
Create your own paywall with full context information:
// Context-aware paywall with fluent API
ContentView()
.withPurchases(products: products)
.withPaywall { context in
VStack {
Text("Upgrade to unlock \(context.triggeredBy ?? "premium features")")
ForEach(context.availableProducts, id: \.self) { product in
Button(product.displayName) {
Task {
try await InAppKit.shared.purchase(product)
}
}
}
if let recommended = context.recommendedProduct {
Text("Recommended: \(recommended.displayName)")
}
}
}
The PaywallContext
provides rich information about how the paywall was triggered:
public struct PaywallContext {
public let triggeredBy: String? // What action triggered this
public let availableProducts: [StoreKit.Product] // Products that can be purchased
public let recommendedProduct: StoreKit.Product? // Best product recommendation
}
InAppKit provides several built-in UI components:
PaywallView
- Modern, animated paywall with product selectionPurchaseRequiredBadge
- Premium crown badge overlayTermsPrivacyFooter
- Configurable footer for terms and privacyFeatureRow
- Styled feature list rowsModernProductCard
- Product selection cards
// Add premium badge to any view
MyCustomView()
.withTermsAndPrivacy()
// Use paywall directly
PaywallView()
// Built-in premium badge appears automatically with .requiresPurchase()
// Basic - any premium purchase required
.requiresPurchase()
// Specific feature required
.requiresPurchase(AppFeature.export)
// Only when condition is true
.requiresPurchase(when: fileCount > 10)
// Combine feature + condition
.requiresPurchase(AppFeature.export, when: fileSize > 5.mb)
What happens: Premium features show a badge, then display your paywall when tapped.
InAppKit.shared - Handles all the StoreKit complexity
// Check what user owns
InAppKit.shared.hasAnyPurchase
InAppKit.shared.isPurchased("com.app.pro")
// Manual purchase (usually not needed)
await InAppKit.shared.purchase(product)
Two View Modifiers:
.withPurchases("product-id")
- Set up your products.requiresPurchase()
- Gate any feature
Available Variants:
// Simple: Just a product ID
.withPurchases("com.app.pro")
// Advanced: Products with specific features
.withPurchases(products: [Product("com.app.pro", AppFeature.allCases)])
// E-commerce App Example
ContentView()
.withPurchases(products: [
Product("com.shopapp.basic", [AppFeature.trackOrders, AppFeature.wishlist]),
Product("com.shopapp.plus", [AppFeature.trackOrders, AppFeature.wishlist, AppFeature.fastShipping]),
Product("com.shopapp.premium", AppFeature.allCases)
])
.withPaywall { context in
ShopPaywallView(context: context)
}
// Productivity App Example
ContentView()
.withPurchases(products: [
Product("com.prodapp.starter", [AppFeature.basicProjects]),
Product("com.prodapp.professional", [AppFeature.basicProjects, AppFeature.teamCollaboration, AppFeature.advancedReports]),
Product("com.prodapp.enterprise", AppFeature.allCases)
])
// Media App Subscription Tiers
ContentView()
.withPurchases(products: [
Product("com.mediaapp.monthly", [AppFeature.hdStreaming, AppFeature.downloads]),
Product("com.mediaapp.annual", [AppFeature.hdStreaming, AppFeature.downloads, AppFeature.offlineMode, AppFeature.familySharing])
])
Features are automatically registered when you use the fluent API, but you can also register them manually:
InAppKit.shared.registerFeature(
AppFeature.advanced,
productIds: ["com.app.pro"]
)
Create your own premium gating logic:
extension View {
func myCustomPremium<T: Hashable>(_ feature: T) -> some View {
self.modifier(MyPremiumModifier(feature: feature))
}
}
InAppKit handles errors gracefully and provides debugging information:
// Check for errors
if let error = InAppKit.shared.purchaseError {
// Handle purchase error
}
// Purchase states
if InAppKit.shared.isPurchasing {
// Show loading state
}
Enable detailed logging to debug StoreKit issues:
// InAppKit uses OSLog with category "statistics"
// Filter in Console.app or Xcode console for "statistics" messages
- Products must be configured in App Store Connect before testing
- Test with Sandbox accounts during development
- Features are automatically registered when using the fluent API
- Debug builds provide helpful warnings for unregistered features
#if DEBUG
// Test purchases in development
InAppKit.shared.simulatePurchase("com.myapp.pro")
InAppKit.shared.clearPurchases() // Reset for testing
#endif
InAppKit follows Apple's privacy guidelines:
- No personal data collection
- All transactions handled by StoreKit
- Local feature validation only
- No analytics or tracking
User Mindset: "I want to create something amazing"
// Problem: User creates something, wants to share/export without limitations
enum CreativeFeature: String, InAppKit.AppFeature {
case removeWatermark = "no_watermark"
case hdExport = "hd_export"
case premiumFilters = "premium_filters"
case cloudStorage = "cloud_storage"
}
// Solution: Let them create first, then offer enhancement
ContentView()
.withPurchases(products: [
Product("com.creative.pro", CreativeFeature.allCases)
])
.withPaywall { context in
CreativePaywallView(triggeredBy: context.triggeredBy)
}
User Mindset: "I need this to work better/faster"
// Problem: User accumulates data, needs more power/space
enum ProductivityFeature: String, InAppKit.AppFeature {
case unlimitedItems = "unlimited_items"
case advancedSearch = "advanced_search"
case teamSync = "team_sync"
case prioritySync = "priority_sync"
}
// Solution: Usage-based upgrades feel natural
Button("Add Project") {
addProject()
}
.requiresPurchase(ProductivityFeature.unlimitedItems, when: projectCount > 5)
User Mindset: "I want more fun/content"
// Problem: User enjoys experience, wants more
enum EntertainmentFeature: String, InAppKit.AppFeature {
case premiumContent = "premium_content"
case noAds = "ad_free"
case earlyAccess = "early_access"
case specialFeatures = "special_features"
}
// Solution: Offer "more of what they love"
ContentView()
.withPurchases(products: [
Product("com.game.premium", EntertainmentFeature.allCases)
])
User Mindset: "I need this for my business success"
// Problem: User needs professional features for work
enum BusinessFeature: String, InAppKit.AppFeature {
case teamAccounts = "team_accounts"
case advancedReports = "advanced_reports"
case apiAccess = "api_access"
case prioritySupport = "priority_support"
}
// Solution: Clear business tiers
ContentView()
.withPurchases(products: [
Product("com.business.professional", [BusinessFeature.advancedReports, BusinessFeature.prioritySupport]),
Product("com.business.enterprise", BusinessFeature.allCases)
])
// User has data/content โ natural to protect/enhance it
.requiresPurchase(AppFeature.backup, when: userContentCount > 20)
// User identity drives purchase โ business features feel necessary
.requiresPurchase(AppFeature.clientSharing, when: isBusinessUser)
// User reaches limitation โ upgrade removes friction
.requiresPurchase(AppFeature.moreStorage, when: storageUsed > freeLimit)
// User enjoys free features โ wants enhanced experience
.requiresPurchase(AppFeature.premiumContent)
import SwiftUI
import InAppKit
// Define app features aligned with business tiers
enum AppFeature: String, InAppKit.AppFeature {
// Basic tier features
case basicFilters = "basic_filters"
case cropResize = "crop_resize"
// Pro tier features
case advancedFilters = "advanced_filters"
case batchProcessing = "batch_processing"
case cloudStorage = "cloud_storage"
// Professional tier features
case rawSupport = "raw_support"
case teamCollaboration = "team_collaboration"
case prioritySupport = "priority_support"
// Enterprise tier features
case apiAccess = "api_access"
case whiteLabeling = "white_labeling"
case ssoIntegration = "sso_integration"
}
@main
struct PhotoEditApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.withPurchases(products: [
// Freemium: Basic features included free
Product("com.photoapp.pro", [
AppFeature.advancedFilters,
AppFeature.batchProcessing,
AppFeature.cloudStorage
]),
Product("com.photoapp.professional", [
AppFeature.advancedFilters,
AppFeature.batchProcessing,
AppFeature.cloudStorage,
AppFeature.rawSupport,
AppFeature.teamCollaboration,
AppFeature.prioritySupport
]),
Product("com.photoapp.enterprise", AppFeature.allCases)
])
.withPaywall { context in
PhotoAppPaywallView(
triggeredBy: context.triggeredBy,
products: context.availableProducts
)
}
}
}
}
struct ContentView: View {
@State private var imageCount = 1
@State private var isTeamMember = false
var body: some View {
VStack(spacing: 20) {
// Always free - basic features
Button("Apply Basic Filter") { applyBasicFilter() }
Button("Crop & Resize") { cropAndResize() }
// Pro tier gating
Button("Advanced AI Filter") { applyAIFilter() }
.requiresPurchase(AppFeature.advancedFilters)
Button("Batch Process") { batchProcess() }
.requiresPurchase(AppFeature.batchProcessing, when: imageCount > 5)
// Professional tier gating
Button("Edit RAW Files") { editRAW() }
.requiresPurchase(AppFeature.rawSupport)
Button("Team Collaboration") { openTeamPanel() }
.requiresPurchase(AppFeature.teamCollaboration, when: isTeamMember)
// Enterprise tier gating
Button("API Access") { configureAPI() }
.requiresPurchase(AppFeature.apiAccess)
}
}
}
We welcome contributions! Here's how to get started:
- Fork the repository
- Clone your fork:
git clone https://github.com/yourusername/InAppKit.git
- Create a feature branch:
git checkout -b feature/amazing-feature
- Make your changes and add tests
- Run tests:
swift test
- Commit your changes:
git commit -m 'Add amazing feature'
- Push to your branch:
git push origin feature/amazing-feature
- Open a Pull Request
- Follow Swift API Design Guidelines
- Use meaningful variable and function names
- Add documentation comments for public APIs
- Maintain backward compatibility when possible
Please use GitHub Issues to report bugs or request features:
- Bug Reports: Include steps to reproduce, expected vs actual behavior
- Feature Requests: Describe the use case and proposed solution
- Questions: Check existing issues first, then create a new discussion
If InAppKit helps your project, please consider:
- โญ Star this repository
- ๐ Report bugs and suggest features
- ๐ Improve documentation
- ๐ฌ Share your experience with the community
This project is licensed under the MIT License - see the LICENSE file for details.
- Built on Apple's StoreKit 2
- Inspired by SwiftUI's declarative approach
- Designed for modern iOS development
InAppKit - Because in-app purchases shouldn't be complicated. ๐
Made with โค๏ธ by the TDDWorks team